Reference

Best practices

Idempotency, pagination, consent, approvals, and safe retries.

Cross-cutting guidance that applies across every guide, collected in one place instead of repeated in each.

Idempotency

Idempotency-Key is an optional request header on the handful of endpoints that are slow, expensive, or dangerous to duplicate (message generation, test sends, workflow enrollment, campaign send). It's not decorative — here's exactly what it does server-side:

  1. First request with a given key: processed normally, and the response is

cached against (project, operation, key) for 24 hours.

  1. Same key again, first request already finished: the cached response is

replayed byte-for-byte, with an Idempotent-Replayed: true response header so you can tell it happened.

  1. Same key again, first request still in flight: 409 Conflict with a

Retry-After: 2 header — back off and retry rather than assuming failure.

  1. A malformed key (must be 1–200 URL-safe characters) is rejected up front

with 422 before anything runs.

Derive the key from something stable in your own data, not a random UUID generated fresh on every attempt — a random key defeats the entire point, since a genuine retry needs to reuse the *same* key as the original attempt to be recognized as a retry. ${workflowId}:${nodeId}:${locale} (drafting one journey email) and send-${campaignId} (sending a campaign) are the patterns used throughout these guides.

Pagination

List endpoints that can return more than a page use a consistent envelope:

JSON
{
  "data": [ "..." ],
  "page": { "nextCursor": "eyJpZCI6Ii4uLiJ9", "hasMore": true, "total": 4213 }
}

Request the next page with page[after]=<nextCursor>; stop when hasMore: false. nextCursor is opaque — don't parse it or construct one yourself, and don't assume it survives across page[size] changes mid-walk. There's no separate offset/limit style pagination anywhere in this API.

Design for the review model

This is the one that actually changes how you should architect an integration, not just a call-order detail. Because every generative or delivery-adjacent endpoint stops at a reviewable state (proposed/draft/review) rather than going live immediately:

  • Don't build a pipeline that assumes one call = done. A "create a

journey" integration is realistically three-plus calls across two review gates (structure approval, then per-locale email confirmation), not one.

  • Surface what's pending, not just what succeeded. A 201 from

createWorkflowProposal means a draft was created, not that anything is live — track status/approvalStatus and show your own users what's still waiting on review.

  • Never try to route around it. There's no force: true or elevated

scope that skips the review gate — agentAutonomyLevel (from GET /agent_manifest) controls how much an agent may *propose*, never whether a human confirms before delivery. Treat that as a hard boundary worth designing for, not friction to engineer around.

Don't store marketing consent as a field in contact_ingestion's properties. Record it through the dedicated consent endpoint instead — it writes to an append-only ledger that Delivery::Eligibility actually checks before every send (missing or revoked consent blocks the send; a plain properties field, however named, is never treated as consent evidence).

The upside of the ledger model: if you grant consent for a contact who's currently stuck (blocked, reason consent_missing) on an active journey, Audryo automatically retries that enrollment the moment consent is recorded — you don't need to detect the block and re-enroll them yourself.

Handle errors by code, not message

JSON
{ "error": { "code": "audience_suggestion_invalid", "message": "...", "requestId": "..." } }

code is the stable contract; message is prose meant for a human and can be reworded without notice. Branch your error handling on code. When reporting a problem to us, include requestId (also on the X-Request-Id response header) — that's what we look up in logs.

Batch sizes and scopes

  • Contacts: up to 5,000 per contact_ingestion call; prefer batches of a

few hundred so one bad record doesn't force resending thousands of good ones.

  • Events: up to 500 per event_ingestion call.
  • Audience conditions: up to 25 top-level entries per group.
  • Request the narrowest scope that does the job (read / write /

deliver) — see API introduction. A sync job never needs deliver.

There's currently no documented hard rate limit; batch sensibly and treat 5xx responses as retryable with backoff regardless.