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:
- First request with a given key: processed normally, and the response is
cached against (project, operation, key) for 24 hours.
- 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.
- Same key again, first request still in flight:
409 Conflictwith a
Retry-After: 2 header — back off and retry rather than assuming failure.
- 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:
{
"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
201from
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: trueor 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.
Consent is a ledger, not a property
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
{ "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_ingestioncall; prefer batches of a
few hundred so one bad record doesn't force resending thousands of good ones.
- Events: up to 500 per
event_ingestioncall. - 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.