The call contract — rules every operation shares
A few rules hold for every operation here, whatever resource you are working on: how much you may call, what a write does to the fields you leave out, and where current behaviour departs from both. Operation pages link here instead of repeating them — read this once, before you write data or loop calls.
Rate limit — 60 requests a minute, per API key
The budget is 60 requests in any 60-second window, per API key, shared across every operation — not per endpoint. Budget per endpoint and you will strand yourself mid-task: sixty reads of one list exhaust the minute for everything else, including the write you have already told the user is coming.
When you are blocked, Retry-After gives the wait in seconds — wait it out. A blocked request
still occupies its slot in the window, so retrying immediately extends the outage instead of ending
it; the only thing that clears it is not calling. When Retry-After is missing, or you have lost
track of what you spent, sixty seconds with no call of any kind, counted from your last one,
always clears the window — the safe move whenever things seem stuck. Every 429 here carries
these same instructions, so what you read mid-flight and what you read here agree.
Spend the budget deliberately: take the largest page you will actually use — one limit: 30 read
costs a third of three limit: 10 reads (schemas/pagination) — prefer one
bulk write to a hundred single ones, and pace any loop of per-record calls. A few operations spend
more than one request per call; most are one-to-one — don't budget to the exact request, the 429
tells you when the window is spent.
Writing — omit, clear, or set
Every patch_* operation applies only the fields you supply:
- Omit a field and its saved value is preserved. Send only what changes.
""clears a string field. It then reads back as""; a field that was never set reads back asnull.nullis rejected with422— "cannot be null. To skip updating this field, omit it from the request." Omit the field instead.- Non-string fields (integer, boolean, date) cannot be cleared once set —
""is rejected there the same asnull. Set a valid replacement value instead.
prospect/post_bulk merges by email under the same rules — supplied
fields overwrite, omitted fields are preserved, "" clears — with one dangerous difference:
null is not rejected there, it is silently dropped. Nothing is applied, nothing is cleared,
and the response still counts the prospect as updated, so a null is indistinguishable from a write
that worked. Never send one. A call carrying more than 100 rows is rejected outright — split the
import.
Freshness — writes here appear at once, web-app changes can lag a few minutes
Everything you write through these operations is visible to your very next read here. Changes someone
makes in the web app are not: sender/get (its search filter included),
campaign/get and
campaign/get_sequences_by_id can take a few minutes to show
an object created or edited there — two identical sender/get searches ~7 minutes apart went from an
empty page to the full roster of 30 senders. So a list read that cannot find something the user says
exists is not evidence it is gone: read the object by id where you have one
(campaign/get_by_id and
sequence/get_followups_by_id do not lag), otherwise wait a
couple of minutes and read the list again before reporting it missing.
Known deviations
Current behaviour that contradicts the rules above. Both are known; work around them until they change.
- sender/patch_by_id resets omitted booleans to
false. A boolean you leave out of that patch comes back off —warmupmost consequentially. Re-send every boolean you want to keep, in the same call. - Filters are not validated. campaign/get's
statusaccepts a value that is not a real status and answers200with an empty list instead of an error. An empty filtered page therefore proves only that nothing matched — never that your filter value was valid. Before reporting "there are none", re-run without the filter, and spell filter arguments exactly as the operation's Arguments table lists them (pageQuery.status,pageQuery.includeArchived).