Skip to Content
LoadersSAP Emarsys

SAP Emarsys Loader

The SAP Emarsys loader pulls marketing data out of SAP Emarsys Customer Engagement and lands it in your data warehouse: contacts and contact-list membership, segment definitions and membership, email campaigns and their launches, aggregate response metrics, and per-recipient delivery outcomes.

It reads through the Emarsys Core API v3 using OAuth 2.0 client credentials. This page is explicit about what Emarsys’s API can and cannot do, because two of its limits change how you should schedule the loader: contacts can only be loaded as a full refresh, and campaigns are incremental on their creation date only. Both are explained under Sync Modes.

Prerequisites

  • An SAP Emarsys Customer Engagement account.
  • An API credential (client ID + client secret) created by an account owner under Management > Security Settings > API Credentials. See Authentication.
  • The API credential must have permission for every function you intend to read. Emarsys permissions are per API user, and a missing one returns HTTP 403 rather than an empty result.
  • A contact list, if you want the contacts or contact_list_members streams. Emarsys has no endpoint that lists every contact in an account, so the loader enumerates contacts from a contact list; most accounts keep an “All contacts” list for exactly this purpose. The other streams do not need one.
  • A connected Warehouse with write permissions on the target schema.

Authentication

The loader uses OAuth 2.0 client credentials against the Emarsys v3 API.

Creating an API credential

  1. Sign in to Emarsys as an account owner.
  2. Open Management > Security Settings > API Credentials.
  3. Create a new API credential and grant it the permissions for the streams you want (contacts, contact lists, segments, email campaigns, email reporting).
  4. Copy the Client ID and Client Secret. The secret is displayed exactly once — if you lose it you must create a new credential.
  5. In Zeotap, paste them into the Client ID and Client Secret fields.

The loader exchanges these for a short-lived access token at https://auth.emarsys.net/oauth2/token and sends it as Authorization: Bearer <token> on every request. The token is cached for its full advertised lifetime minus a five-minute safety margin, because the token endpoint is limited to 50 requests per minute and 200 per hour per client ID — far tighter than the data API itself.

Why not WSSE?

Emarsys’s older X-WSSE header mechanism (API user name + secret) is deprecated, with its final sunset at the end of 2026. This loader deliberately implements only the v3 OAuth path. If your account still uses a WSSE API user, create an OAuth API credential before connecting — the WSSE credentials will not work here.

Rate limiting, authorization, and authentication are scoped to the API credential, not the account. Give Zeotap its own credential so a loader run cannot consume another integration’s quota.

Configuration

SettingDescriptionDefault
Client IDOAuth 2.0 client ID from your Emarsys API credential— (required)
Client SecretOAuth 2.0 client secret (shown only once at creation)— (required)
Contact ListThe contact list the contacts and contact_list_members streams read. Picked from a live dropdown of the lists in your account. Required only for those two streams — those runs fail with a clear message if it is unset; every other stream ignores it
Contact Field IDsEmarsys numeric field ids to hydrate per contact. Documented system fields become readable columns (1first_name, 3email, 4date_of_birth); anything else lands as field_<id>1, 2, 3, 4, 11, 14, 15, 31, 37, 48
Field LanguageLanguage code used when reading contact field metadataen
Email Campaign IDsRestricts the launch, response-summary and delivery-status streams to these campaign ids. Leave empty to derive them from the account— (all in-scope campaigns)
Campaign Start DateEarliest campaign creation date to read (YYYY-MM-DD). Emarsys returns the entire campaign list in one response with no pagination, so this is the only way to bound it— (no bound)
Segment IDsRestricts the segment_contacts stream to these segment ids. Leave empty to read every segment— (all segments)
Page SizeRows requested per page ($top on contact-id enumeration, limit on offset-paginated endpoints). Range 1–100,000, matching Emarsys’s documented $top bounds10000
Contact Hydration Batch SizeContact ids hydrated per request. Emarsys rejects a larger batch outright1000 (Emarsys’s cap)

Available Streams

StreamDescriptionKey columnsIncremental cursor
contact_fieldsContact field dictionary — the metadata you need to interpret field idsid, name, application_type, string_id— (full refresh)
contact_listsEvery contact list in the accountid, name, created, type— (full refresh)
contact_list_membersContact ids currently in the configured contact listcontact_list_id, contact_id— (full refresh)
contactsContacts in the configured list, hydrated with the selected field idsid, uid, email, first_name, last_name, …— (full refresh only, see below)
segmentsSegment (filter) definitionsid, name, type, tags, criteria_types— (full refresh)
segment_contactsContacts currently matching each selected segmentsegment_id, contact_id, raw— (full refresh)
email_campaignsEmail campaign definitionsid, name, subject, status, created, contactlist, filtercreated (day granularity)
email_campaign_launchesLaunches of each in-scope campaignemail_id, id, done, launch_date— (full refresh)
campaign_response_summaryAggregate deliverability and engagement counters per launchcampaign_id, launch_id, sent, opened, total_clicked, hard_bounced— (full refresh)
email_delivery_statusPer-recipient delivery outcome for each launchemail_id, launch_id, user_id, status, bounce_reasonlast_id (keyset)
external_eventsExternal event definitions used to trigger campaignsid, name, created— (full refresh)

Default selected streams: contact_lists, contacts, email_campaigns.

Column type choices worth knowing

  • Timestamps are stored as text. Emarsys documents created, deleted and launch_date only as prose (“The date of creation.”) and its own examples show two different layouts. Rather than guess a format and silently drop rows, these columns land as strings — cast them in your models to match your data.
  • Contact date fields are real dates. Emarsys states the only format its contact system date fields accept is YYYY-MM-DD, so date_of_birth (field 4), anniversary (40) and date_of_first_registration (48) land as DATE. An empty value becomes NULL.
  • Some numeric-looking fields are strings. Campaign id, browse, launch done and contact opt_in are documented as strings, and opt_in in particular is an encoded tri-state (1 = true, 2 = false, empty = unknown) rather than a boolean. They land as text so no value is lost.
  • tags, criteria_types, features and raw hold JSON text. Query them with your warehouse’s JSON functions.
  • segment_contacts.raw exists because Emarsys does not publish the response body of its segment-contacts endpoint. Bare ids populate contact_id and leave raw empty; if your account returns richer objects, the whole object is preserved in raw so nothing is lost.

Sync Modes

StreamModeNotes
email_campaignsIncremental or full refreshCursor is the creation date at day granularity
email_delivery_statusIncremental or full refreshKeyset cursor, tracked independently per campaign + launch
everything elseFull refreshEmarsys exposes no filterable modification timestamp for them

Why contacts is full refresh only

This is the most important limitation on this page. No synchronous Emarsys contact endpoint can be filtered by a modification date. The contact query and contact-data endpoints accept only an equality filter on a field value plus limit/offset — there is no since, no range operator, and no sort parameter anywhere in the API. There is therefore no honest cursor to offer, and the loader declares none rather than faking one and producing silent gaps.

Emarsys does have an asynchronous “Export Updated Contacts” job, and it looks like the answer, but it is not: Emarsys’s own documentation states that a contact updated via data import is not exported at all — for any changed field, including the email address and opt-in status. For most accounts, imports are the majority of contact churn, so a cursor built on it would quietly miss most updates. It also only accepts whole-day windows, is limited to 20 fields, and reports its dates in the Vienna time zone.

What to do instead: run contacts as a full refresh on a schedule your contact volume supports (nightly is typical), and drive change detection in your models by comparing snapshots. Use contact_list_members — which is cheap and cleanly paginated — if you only need membership rather than attributes.

Why email_campaigns re-reads a day

fromdate filters on the campaign creation date and accepts a date only, and Emarsys does not document whether the boundary is inclusive or exclusive. To avoid dropping a day of campaigns on every run, each incremental run starts one day before the stored watermark and lets the warehouse deduplicate, so the re-read window stays two days wide rather than growing.

The watermark is the latest campaign creation date the run actually observed — never the wall clock. A run whose window returns no campaigns publishes no watermark at all, so the previous one is retained. This matters because created is in the account’s timezone while a server clock is UTC: advancing the watermark to “today” on an account behind UTC can push it past campaigns that have not been created yet locally, and those campaigns would then never be read.

Also note created is a creation date, not a modification date: a campaign whose subject or status changes later will not reappear in an incremental run. Schedule a periodic full refresh of email_campaigns if you report on campaign status.

How email_delivery_status resumes

Emarsys’s lastId parameter is documented as exclusive — “the piece of data lastid refers to is not included” — so this is the one stream where resuming skips no rows and duplicates none. Because the position is per campaign and launch, the saved cursor is a small map of those positions rather than a single value. A run that adds a new campaign simply starts that campaign from the beginning.

How It Works

  1. Token minting. The loader exchanges the client credentials for an access token once, then reuses it across every stream and page for its full lifetime minus five minutes. If Emarsys answers 401 mid-run, the token is re-minted once and the page retried.
  2. Contacts are enumerated, then hydrated. Emarsys has no “list all contacts with all fields” endpoint. The loader walks the configured contact list’s ids using the one endpoint with a documented pagination terminator, then fetches attributes for those ids in batches of at most 1,000 (Emarsys’s hard cap) and writes each batch as it arrives.
  3. Three different pagination styles. Contact-id enumeration uses $skiptoken/$top and stops when the API reports no further chunk. Segment contacts use limit/offset and stop on an empty or short page — Emarsys publishes no total or “has more” flag anywhere, so that is the only available signal. Delivery status uses the exclusive lastId keyset. Every loop also has a hard ceiling so an unexpected response cannot spin forever.
  4. Unpaginated endpoints are split into batches. Several Emarsys endpoints (contact lists, segments, campaigns, launches, events) accept no paging parameters and return the whole collection in one response. The loader writes those rows in fixed-size batches rather than one enormous batch.
  5. Every response is checked for an application error. Emarsys returns a replyCode alongside HTTP 200, and a non-zero value is a failure even though the status says success. The loader surfaces the code and message instead of writing an empty table. HTTP 202 (a segment still being evaluated) is polled rather than treated as an error.
  6. Cursor checkpointing. For email_campaigns the new watermark is attached only to the final batch of a run, so a run that fails half way checkpoints nothing and the next run re-reads the whole window instead of skipping unwritten rows. For email_delivery_status each page’s position is checkpointed after its rows are written.

Rate Limits

SurfaceDocumented limit
Core API (api.emarsys.net/api/v3)1,000 requests per minute per API credential
OAuth token endpoint50 per minute and 200 per hour per client ID; 10,000 per day per account
Email reporting endpoints1,000 per minute per API user, shared across all reporting endpoints
Request payload10 MB

Emarsys returns 429 Too Many Requests when you exceed the Core API limit and publishes X-RateLimit-Limit, X-Ratelimit-Remaining and X-RateLimit-Reset headers. It does not publish a Retry-After header, so the loader derives its wait from X-RateLimit-Reset when present and otherwise backs off on a fixed interval, retrying a bounded number of times before failing the run.

Two practical consequences:

  • Give the loader its own API credential. The 1,000/minute budget is per credential, so sharing one with another integration makes both unpredictable.
  • The campaign fan-out streams are request-heavy. email_campaign_launches and email_delivery_status make at least one request per campaign (and per launch, for delivery status); campaign_response_summary batches up to 3,000 campaign ids per request, so it stays at one or two calls. Set Campaign Start Date or list explicit Email Campaign IDs to keep the run bounded; the loader refuses to fan out over more than 5,000 campaigns in a single run rather than silently truncating.

Troubleshooting

IssueCause and fix
”token endpoint rejected the client credentials”The client ID or secret is wrong, or the credential was deleted. The secret is shown only once at creation — create a new API credential under Management > Security Settings > API Credentials if it was lost. WSSE API user names and secrets will not work; this loader uses the v3 OAuth credential.
”token endpoint rate limited (HTTP 429)“You exceeded 50 mints/minute or 200/hour on that client ID. Almost always means another integration shares the credential — give Zeotap its own.
HTTP 403 with a permission messageNot a credential problem. The API credential lacks permission for that specific Emarsys function; no retry will fix it. Ask an account owner to enable the function for this API user, or deselect the stream.
”API error replyCode 2004 / 2005 / 2006 / 2007” on a run that looks successfulEmarsys returns these with HTTP 200. They mean an invalid key field id, field id, or field value — usually a Contact Field ID that does not exist in your account. Check the contact_fields stream for the valid ids.
contacts is empty, or has far fewer rows than expectedThe configured Contact List does not contain the contacts you expect. Membership is what the loader enumerates — a list of 500 people yields 500 rows regardless of how many contacts the account holds. Load contact_lists first and pick a list that covers everything you need.
contacts never picks up updates between runsExpected: contacts is full refresh only, because Emarsys has no date-filterable contact endpoint (see Sync Modes). Schedule a full refresh; do not expect an incremental cursor to appear.
New or edited campaigns are missing from email_campaignsThe cursor is the creation date, so an edited campaign does not reappear. Run a periodic full refresh. If genuinely new campaigns are missing, widen Campaign Start Date — the filter is day-granularity and its inclusivity is undocumented, which is why each run deliberately re-reads the previous day.
”the account has N email campaigns, more than the 5000 one run fans out over”The launch, response-summary and delivery-status streams make at least one request per campaign. Set Campaign Start Date to narrow the window or list explicit Email Campaign IDs.
”was still being evaluated after 30 polls” on segment_contactsEmarsys evaluates segment membership on demand and answers HTTP 202 while it works. A very large segment can take longer than the loader will wait. Narrow Segment IDs to the segments you actually need, or retry when the platform is less busy.
segment_contacts rows have contact_id empty but raw populatedEmarsys does not publish this endpoint’s response body. The loader stores whatever it receives in raw rather than discarding it. Extract the identifier you need from raw with your warehouse’s JSON functions and let us know the shape you see.
campaign_response_summary numbers change between runsExpected. These are aggregates that Emarsys restates as late opens, clicks and bounces arrive, which is why the stream is always a full refresh. Report from the latest snapshot.
sent (or another counter) is NULL rather than 0Emarsys’s own examples return null for counters that are not yet available for a launch. NULL means “not reported”, which is genuinely different from zero.
Cannot reach Emarsys at all / TLS errorEmarsys requires TLS 1.2 and explicitly rejects requests addressed to an IP address rather than api.emarsys.net. Check any outbound proxy in front of the loader.
Duplicate rows after a failed runFull-refresh streams replace their table, but incremental streams are at-least-once by design (the email_campaigns window deliberately overlaps by a day). Deduplicate on id — or on email_id + launch_id + user_id for delivery status — in your models.

Next Steps

  • Create a model that joins contacts to contact_list_members and segment_contacts to build a segment-membership fact table
  • Join email_delivery_status to email_campaign_launches and email_campaigns for per-recipient campaign attribution
  • Compare consecutive contacts snapshots to detect attribute changes that Emarsys’s API cannot report incrementally
  • Sync enriched audiences back out to your activation tools
Last updated on