Skip to main content
Learn these once and every endpoint behaves the way you expect.

Fields are snake_case, and typos are loud

Requests and responses use snake_case. Unknown request fields are rejected, not ignored — a camelCase body fails with 400 VALIDATION_ERROR rather than silently doing nothing.
That’s deliberate. A misspelled optional field is a bug you want to hear about on the first call, not after a campaign goes out with the wrong settings.

Every collection pages the same way

List endpoints return one envelope:
Pass next_cursor back as cursor to get the next page. null means you’re on the last one. limit defaults sensibly and caps at 100.

Slow work returns a Job

Anything that can’t finish inside a request returns 202 with a Job instead of making you wait: sourcing leads, enrichment, generating copy, buying domains, provisioning mailboxes, chat messages. Poll GET /jobs/{job_id} honoring next_poll_at, or subscribe to job.* webhooks and skip polling entirely.

The Job object

Statuses, polling, cancelling, credits, and needs_input.

Destructive calls preview first

Operations that delete things or spend money return a preview of what would happen unless you pass confirm=true. Nothing is destroyed and no card is charged on the preview call.
confirm=true applies to deleting a list, project, or campaign, removing a campaign sender, clearing an exclusion list or a template, and buying domains, where the preview comes back priced. Some operations use a dry_run body flag for the same idea: launching a campaign with {"dry_run": true} reports exactly the gates a real launch would check, and removing or stopping a person reports what it would affect.

Retries are safe if you ask for it

Any POST may carry an Idempotency-Key header. Replaying the same key with the same body returns the original result instead of doing the work twice.
Use one key per logical operation — a UUID generated when your job starts, not per HTTP attempt.

Errors have one shape

error is for humans, code is for your switch statement. Branch on code — the message text can change. handoff appears when a person can fix the problem in the Origami dashboard but your code can’t: connecting a mailbox, re-authorizing one that expired, adding a payment method, upgrading a plan, or confirming a card charge. Forward the URL to whoever owns the account rather than treating it as a hard failure.

Scoping to a project

Keys are parent-wide. Send x-origami-project: <project_id> to act inside a child project instead of the parent:
The header applies to Leads, Send, Jobs, and — within Account — chats and exclusion lists. It is ignored by project management, org reads, senders, domains, mailboxes, webhooks, and API-key routes, which are always parent-scoped.

Rate limits

300 requests/minute per IP and 100/minute per organization, both keyed to the parent org even for project-scoped requests. The binding constraint for agent work is usually concurrent runs, not requests — exceeding that returns 429 CONCURRENT_LIMIT_EXCEEDED. Check where you stand at any time with GET /account/rate-limits. Full details, including response headers, are in authentication.