Skip to Content
Conventions

Conventions

These apply to every endpoint and aren’t repeated on resource pages. Read them once before you write an import script.

Requests and responses

JSON in, JSON out, UTF-8. Send Content-Type: application/json with any body.

  • Fields are snake_case.
  • Ids are prefixed strings: eml_, aud_, sub_, fld_, seg_, top_, cmp_, aut_, aen_, sup_, whe_, whd_, evt_. Treat them as opaque.
  • Timestamps are ISO-8601 UTC with milliseconds: 2026-08-21T09:00:00.000Z.
  • Ignore unknown response fields. Additive changes ship in v1 without a version bump. Breaking changes would be v2.
  • Deletes return 200 with { "id": "...", "deleted": true }, not 204.
  • Another organization’s id is 404, never 403. Existence never leaks.

Unknown fields in a request are dropped, not rejected

An unrecognised top-level field is silently ignored. A payload can look accepted while losing data.

You can’t backdate a contact. created_at in a payload is stripped and the contact gets today’s date. Original signup dates can’t be preserved on import. unsubscribed_at is the exception: it is honoured on create, so opt-out dates survive a migration.

Map every source column to a documented field or into attributes, and tell your user what couldn’t be carried over.

Pagination

Every list endpoint is cursor-paginated. No page numbers, no offsets: offsets break under concurrent writes, which is exactly the migration case.

GET /audiences/aud_123/contacts?limit=100&after=sub_01h...
ParamValues
limit1 to 100. Default 50.
afterOpaque cursor. Pass the previous response’s next_cursor.
{ "data": ["..."], "has_more": true, "next_cursor": "sub_01h..." }

Loop until has_more is false. Order is stable, newest first (created_at DESC, id DESC), and the cursor encodes both.

let after; do { const url = new URL("https://go.day3.app/api/v1/audiences/aud_123/contacts"); url.searchParams.set("limit", "100"); if (after) url.searchParams.set("after", after); const res = await fetch(url, { headers: { Authorization: "Bearer " + process.env.DAY3_API_KEY }, }); const page = await res.json(); handle(page.data); after = page.next_cursor; } while (after);

Idempotency

Every POST accepts:

Idempotency-Key: <your own string, 255 characters or fewer>

A retry with the same key within 24 hours replays the original response, same status and body, with Idempotency-Replayed: true. Always set it on imports and sends.

  • Same key, different body: 409 idempotency_conflict.
  • Two concurrent requests with one key run exactly once. The loser gets 409 idempotency_conflict (“already in progress”) and should retry shortly.
  • A claim abandoned by a crashed request is taken over after five minutes.
  • POST /emails completes its claim in the same transaction that writes the email. A crash leaves either no email (the retry sends it) or an email whose response the retry replays. Never a duplicate send.

PATCH and DELETE are naturally idempotent and need no header.

Rate limits

600 requests per minute per organization, sliding window.

HeaderOn
RateLimit-LimitEvery response
RateLimit-RemainingEvery response
RateLimit-ResetEvery response
Retry-After429 only, in seconds

On 429 rate_limit_exceeded, sleep Retry-After seconds and retry. Don’t back off blindly.

Transactional sends have their own bucket inside that limit: 120 per minute per organization by default, so a burst of password resets can’t starve the rest of the API.

A batch call counts as one request. Importing 50,000 contacts is 50 requests, not 50,000. Never loop single creates for an import. See Contacts.

Plan limits

The API enforces the same rules as the app.

SituationResponse
Free plan, list would exceed 500 subscribers403 plan_limit_reached
Monthly send allowance exhausted403 plan_limit_reached
Free plan, recipient outside your organization403 sandbox_recipient_not_allowed
Sending disabled on the account403 sending_disabled

A batch that would cross the subscriber cap is rejected whole, never partially applied, and the message states the remaining headroom. Re-run the import after upgrading. Don’t retry it, and don’t split the batch to sneak under the cap.

Last updated on