SAP Customer Data Cloud Loader
The SAP Customer Data Cloud loader pulls customer identity data out of SAP Customer Data Cloud (formerly Gigya) into your data warehouse using the accounts.search REST API, plus audit.search for API audit events.
It reads full accounts, lite/email accounts with their subscriptions, communication opt-in state, and the audit log. Profile, consent, and custom-schema containers land as JSON columns, so per-tenant schema customisation needs no configuration in Zeotap.
Password hashes are never imported. SELECT * in SAP Customer Data Cloud returns password.hash and hashSettings; Zeotap builds every query from an explicit field list instead, so hash material is excluded by construction and stripped again defensively before rows are written.
Prerequisites
- An SAP Customer Data Cloud site, and the data center it lives in (US1, EU1, AU1, EU2, CN1, or GLOBAL)
- The site API key from Console → Sites (e.g.
3_mKx…) — this is a public identifier, not a secret - An application key and secret from Console → Admin → Applications, whose permission group includes Search on that site
- A connected Warehouse with write permissions on the target schema
Two things are worth confirming before you start:
- The application key must be allowed to open a cursor. SAP documents cursor paging (
openCursor) as available only to server-to-server calls carrying an application key and secret. Without a cursor, a query returns at most 5,000 records in total — not per page — so a full extract is impossible. Zeotap’s connection test probes this explicitly. - Partner secrets are not supported. SAP retired partner-secret authentication on 31 January 2023, and
audit.searchrejects it outright. Use an application key.
Authentication
Application Key & Secret (recommended)
- In the SAP Customer Data Cloud Console, go to Admin → Applications and create (or open) an application
- Copy the application key and generate/copy its secret
- Make sure the application’s permission group grants Search on the site you are importing
- In Zeotap, click Add Loader, select SAP Customer Data Cloud, and choose Application Key & Secret
- Enter the application key as Application (User) Key and the secret as Application Secret
No request signing is needed with an application key — SAP explicitly waives the HMAC signature in this case. All requests are sent over HTTPS; SAP rejects a secret sent over plain HTTP with errorCode 400001.
OAuth 2.0 Client Credentials
Zeotap can instead exchange the same application key and secret for a bearer token:
POST https://oauth2.<data-center>/oauth2/token
grant_type=client_credentials
client_id=<application key>
client_secret=<application secret>
resource=https://api.gigya.comSelect OAuth 2.0 Client Credentials and supply the application key as Client ID and the secret as Client Secret. The token endpoint is derived from the data center you select — you do not enter a URL. Zeotap caches the token until one minute before it expires and re-mints it automatically when SAP answers 403025 Invalid token, retrying the affected page once.
One honest caveat: SAP’s accounts.search reference says openCursor is available only to calls carrying an application key and secret, while SAP’s own OAuth 2.0 documentation demonstrates a cursor scroll authenticated with a bearer token. The two statements conflict and we could not settle it against a live tenant. Zeotap’s connection test opens a real cursor, so if your tenant enforces the stricter reading, the test fails immediately with an explanation rather than silently truncating an import at 5,000 rows. If that happens, switch to Application Key & Secret.
The data center must match the site
There is no discovery API for a site’s data center — SAP’s own guidance is to hover the flag next to the site in the Console. A wrong selection does not produce a “wrong region” error. The site API key simply does not exist in that data center, and the call comes back as 403007 Permission denied, which reads exactly like a missing permission. If your credentials look correct and you still get a permission error, re-check the data center first.
| Data center | Host | Console |
|---|---|---|
| US1 — United States | us1.gigya.com | https://console.gigya.com |
| EU1 — Europe | eu1.gigya.com | https://console.gigya.com |
| AU1 — Australia | au1.gigya.com | https://console.gigya.com |
| EU2 — European Azure | eu2.gigya.com | https://console.eu2.gigya.com |
| CN1 — China | cn1.sapcdm.cn | https://console.cn1.sapcdm.cn |
| GLOBAL — Global site groups only | global.gigya.com | n/a |
Note that CN1 is cn1.sapcdm.cn, not a gigya.com host, and that CN1 and EU2 have separate Consoles — a site created in either is invisible from the global Console.
Configuration
| Field | Required | Default | Description |
|---|---|---|---|
| Data Center | Yes | us1.gigya.com | The data center hosting your site. Must match exactly — see above. |
| Site API Key | Yes | — | The site API key from Console → Sites. Public, not a secret. |
| Account Types | No | full | Which account types the Accounts stream returns: full, lite, or full,lite. |
| Batch Size (LIMIT) | No | 500 | Rows requested per cursor hop. SAP documents a cursor ceiling of 1000 and does not guarantee the batch size actually returned. |
| Request Timeout (ms) | No | 20000 | Server-side search timeout passed to SAP. Documented maximum is 60000. |
| Cursor Safety Lag (seconds) | No | 60 | Incremental watermarks are never advanced past now minus this lag. See Sync Modes. |
| Audit Log Lookback (days) | No | 30 | Cold-start window for the Audit Log stream. Capped at 180 days by SAP. |
Available Streams
| Stream | Source | Key columns | Incremental cursor |
|---|---|---|---|
| Accounts | FROM accounts | UID, created, lastUpdatedTimestamp, isActive, isRegistered, isVerified, loginProvider, plus profile, data, preferences, identities, loginIDs, emails, groups, lastLoginLocation, customIdentifiers, rbaPolicy, addresses as JSON | lastUpdatedTimestamp |
| Email (Lite) Accounts | FROM emailAccounts | token, email, hasFullAccount, hasLiteAccount, channel, plus subscriptions, data, profile as JSON | lastUpdatedTimestamp |
| Communications | FROM communications | UID, topicId, channelId, status, timestamp | None — full refresh only |
| Audit Log | audit.search, FROM auditLog | event_timestamp, callID, endpoint, errCode, uid, apikey, userKey, ip/sourceIP, plus params, httpReq, userAgent, userKeyDetails as JSON | event_timestamp |
Accounts is selected by default.
Why Email Accounts is a separate stream
SAP states plainly that a search from the Accounts object does not return subscriptions. Subscriptions live only under emailAccounts, so they are a separate stream rather than a column on Accounts. Enable both if you need profiles and subscription state together, and join on email — bearing in mind that one lite account can map to several full accounts sharing an email address, so email is not a unique key against accounts.UID.
Nested containers stay JSON
profile, data, preferences, identities, loginIDs, emails, groups, lastLoginLocation, customIdentifiers, rbaPolicy, addresses, and subscriptions are schema-driven and differ per tenant, so they land verbatim as JSON strings and are never flattened into columns. Query them with your warehouse’s JSON functions. This keeps the table shape stable when a customer adds a dataSchema field.
Fields deliberately excluded
| Field | Why |
|---|---|
password, hashSettings | Password hash and hash configuration. Landing these in a warehouse is a security exposure with no analytical value. |
userInfo | Deprecated by SAP; must not be relied upon. |
iRank | Deprecated; always returns 0. |
isLockedOut | Deprecated in favour of lockedUntil, which is imported. |
UIDSignature, signatureTimestamp | Deprecated for server-to-server REST calls. |
The Audit Log stream is a different API
audit.search runs on the audit.<data-center> host, not accounts.<data-center>, and its rules differ: SELECT may only be *, the @timestamp predicate must carry both a lower and an upper bound (without valid timestamps SAP silently returns only the last 30 days), and no window may span more than 6 months. Zeotap sweeps a bounded window per run and abuts the next run’s window to the previous upper bound, so there is neither gap nor overlap. Two further notes: the @timestamp field is renamed event_timestamp in the warehouse because @ is not a portable column name, and calls made with application keys are not audited, so Zeotap’s own reads never appear in the data it imports.
Sync Modes
Full Refresh
Every enabled stream can run full refresh: Zeotap scrolls the entire result set with a cursor and appends every row.
Incremental
Accounts, Email Accounts, and Audit Log support incremental sync. Communications does not — see below.
Accounts and Email Accounts are keyed on lastUpdatedTimestamp, SAP’s epoch-milliseconds field, with the predicate
WHERE lastUpdatedTimestamp >= <stored watermark> ORDER BY lastUpdatedTimestampThree deliberate choices in that one line:
- The integer field, not the ISO string. SAP’s own optimisation guidance says date ranges evaluate far more efficiently against a timestamp field, integer literals have an unambiguous documented form (unquoted), and there is no documented literal format for comparing the ISO-8601
lastUpdatedfield inaccounts.search. Filtering the string field would be guesswork. >=, not>.lastUpdatedTimestampis millisecond-granular and SAP can stamp many accounts inside the same millisecond.>would silently drop every record tied on the boundary, permanently.>=re-reads the boundary millisecond instead, which costs a few duplicate rows in an append-only table — the right side of that trade.lastUpdatedTimestamp, notlastUpdated. SAP documentslastUpdatedTimestampas also being bumped by login events, so it moves at least as often aslastUpdated. For a watermark that must not miss a change, the more frequently updated field is the safe one.
The cursor safety lag, and why it exists
SAP documents a delay between writing account data and that data becoming searchable: 1 second on average, up to 30 seconds. Without a guard, a record modified moments before a run finished would carry a timestamp below the new watermark while still being invisible to the query that set it — and the next run, filtering above the watermark, would never see it. That is a permanent, silent gap.
Zeotap therefore never advances a stored watermark past now minus the Cursor Safety Lag (default 60 seconds). The cost is that the final seconds of a window get re-read on the next run; the benefit is that no write can slip between two runs. Lower it only if you accept that risk, and do not set it below 30 seconds.
Communications is full refresh only
FROM communications exposes a single timestamp field, an ISO-8601 string. It has no epoch-milliseconds twin, and SAP documents no literal format for comparing it in a WHERE clause. Rather than ship a cursor the query language cannot reliably filter on, Zeotap declares the stream full-refresh only. Schedule it less frequently than Accounts if opt-in volumes are large.
Deleted accounts are invisible — plan for it
SAP states that deleted accounts do not appear in queries, and there is no tombstone, isDeleted flag, or deletion feed on accounts.search. A hard-deleted account simply stops being returned.
The consequence: incremental sync can never detect a deletion. A profile deleted in SAP Customer Data Cloud remains in your warehouse forever unless something removes it. The only mitigation is a periodic full refresh into a fresh table, then treating rows absent from the new snapshot as deleted — and even that detects absence rather than deletion, so it cannot distinguish a delete from a row that fell outside the query. If you are using this data to satisfy erasure requests, do not rely on the loader alone; drive deletions from SAP’s own consent and deletion tooling as well.
How It Works
- Cursor open. Zeotap POSTs the query to
https://accounts.<data-center>/accounts.searchwithopenCursor=true. It is always a POST, never a GET: an explicit field list makes the query long enough to breach URL length limits. - Cursor continuation. Every subsequent request carries only the
cursorIdreturned by the previous response — no query, noopenCursor, noLIMIT. PassingqueryandcursorIdtogether is an error in SAP Customer Data Cloud: theWHEREandORDER BYare bound once, at open time, and the server holds the sorted result set for the life of the scroll. - Batches are emitted, never buffered. Each page is written to the warehouse as it arrives. This is required for memory reasons and because the cursor expires after 5 minutes of inactivity between hops — a large batch size plus a slow warehouse write can kill the scroll mid-way. That is why the default batch size is 500 rather than SAP’s 1000 ceiling.
- Termination is guarded three ways. Zeotap stops when
nextCursorIdis absent or empty, or when it equals the cursor just sent, or when a page returns zero results. SAP’s reference documentation, SAP’s own code sample, and SAP KBA 3085264 disagree about which signal actually fires on the last page, so all three are honoured. A short page is explicitly not treated as the end: SAP states the batch size under a cursor is not guaranteed. - Errors are read from the body, not the status line. SAP returns HTTP 200 for application-level errors unless
httpStatusCodes=trueis sent. Zeotap sends that flag and checkserrorCodeon every response unconditionally, because the body remains authoritative. The deprecatedstatusCode/statusReasonfields are ignored. Every error message includes SAP’scallId, which is the first thing SAP support will ask for. - Type fidelity. The six
*Timestampfields are 13-digit epoch millis. Responses are decoded with exact-number semantics so they land as true integers; decoded naively they would arrive as floating point and be written as1.7e+12into an integer column, failing at INSERT. - Up to four streams are read concurrently. One page at a time within each stream, but the loader pipeline runs several streams in parallel — so the searches-per-second budget below is shared across them. See Rate Limits.
Rate Limits
SAP’s Acceptable Use Policy tiers limits by contracted contact volume, and searches are by far the tightest bucket:
| Metric | ≤5M contacts | ≤10M | ≤20M | ≤50M | >50M |
|---|---|---|---|---|---|
| Identity — searches/sec | 10 | 15 | 20 | 30 | 50 |
| Consent — searches/sec | 5 | 5 | 10 | 15 | 20 |
| Identity — logins/sec | 50 | 90 | 130 | 200 | 320 |
| Identity — other API calls/sec | 120 | 180 | 280 | 420 | 630 |
Exceeding a limit returns errorCode 403048 API Rate Limit Exceeded, which Zeotap classifies as retryable and backs off on.
This budget is shared with your production traffic. The search bucket is the same one your own applications draw on, and at the entry tier it can be as low as 5–10 calls per second. Zeotap reads one page at a time within each stream, but the pipeline runs up to four streams concurrently — so with several streams selected you can have up to four concurrent searches against that bucket. Select only the streams you need, and prefer scheduling this loader away from your peak hours. Do not work around this by running several loaders against the same tenant concurrently; a burst of searches can starve the quota your login flows depend on.
SAP does not publish the enforcement windows (burst versus sustained, per-data-center versus per-API-key) — that detail lives in login-walled Knowledge Base Articles 2702625, 2702339, and 3542818. Client-side and per-IP limits are likewise undocumented. Zeotap always sends server-to-server credentials so calls land in the documented server-side bucket rather than the unpublished client-side one.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
errorCode 403007 Permission denied | Either the wrong data center or an application key without Search permission — SAP reports both identically | Confirm the data center by hovering the flag next to the site in the Console, then confirm the application’s permission group grants Search on that site |
errorCode 403010 Invalid Secret or 403003 Invalid request signature | The application secret does not match the application key | Re-copy the secret from Console → Admin → Applications. Do not use an account or partner secret — partner secrets were retired on 31 January 2023 |
| Connection test passes but Verify cursor paging fails | The credentials can search but cannot open a cursor | Switch to the Application Key & Secret method. Without a cursor, SAP caps a query at 5,000 records total, so Zeotap refuses to import rather than truncate silently |
errorCode 403025 Invalid token | The OAuth 2.0 bearer token expired or was invalidated | Handled automatically: Zeotap re-mints the token and retries the page once. Persistent failures mean the client credentials are wrong |
errorCode 403048 API Rate Limit Exceeded | The tenant’s searches/sec budget is exhausted, often by production traffic rather than by the loader | Schedule the loader outside peak hours and do not run multiple loaders against the same tenant concurrently. Zeotap already backs off and retries |
errorCode 400001 / 403006 (HTTPS required) | A secret was sent over plain HTTP | Not reachable through normal configuration — all Zeotap requests use HTTPS. Contact support if you see this |
errorCode 400006 Invalid parameter value | A malformed query, or a LIMIT/timeout outside SAP’s accepted range | Lower Batch Size to 1000 or below and keep Request Timeout at or under 60000 |
errorCode 500001 General Server error on a cursor scroll | A known SAP-side fragility with openCursor on accounts.search (KBA 2889755) | Zeotap treats it as retryable. If it persists, lower the batch size and raise the issue with SAP quoting the callId from the error message |
| Import stops part-way with an expired-cursor error | More than 5 minutes elapsed between two cursor hops, usually a slow warehouse write | Lower Batch Size so each page’s write finishes well inside the window |
| Rows appear duplicated after an incremental run | Expected. The cursor uses >= and the safety lag re-reads the boundary window, both to avoid dropping records | De-duplicate downstream on UID (or email for lite accounts) keeping the highest lastUpdatedTimestamp |
Accounts imported, but the subscriptions column is missing | A search from the Accounts object does not return subscriptions | Enable the Email (Lite) Accounts stream, which is the only source of subscription state |
| Deleted profiles are still in the warehouse | There is no tombstone — deleted accounts simply stop appearing in queries | Run a periodic full refresh into a fresh table and treat absent rows as deleted. Incremental sync cannot detect deletions at all |
| A record edited seconds ago is missing from a run | SAP takes up to 30 seconds to make a write searchable | Expected. The next run picks it up, which is exactly what the Cursor Safety Lag protects. Do not lower the lag below 30 seconds |
| Audit Log returns roughly 30 days when a longer window was configured | The @timestamp bounds were rejected, so SAP fell back to its 30-day default | Keep Audit Log Lookback at or under 180 days; SAP caps any audit window at 6 months |
| Zeotap’s own API calls are absent from the Audit Log | Calls made with application keys are not audited by SAP | Expected and not configurable |
| A per-tenant custom field is not a warehouse column | Custom dataSchema and profileSchema fields stay inside the data and profile JSON columns | Query them with your warehouse’s JSON functions; the table shape stays stable when the schema changes |
| A CN1 (China) site is not visible in the Console | CN1 and EU2 have separate Consoles | Use https://console.cn1.sapcdm.cn, and select cn1.sapcdm.cn — not a gigya.com host — as the data center |