MCP

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 as null.
  • null is rejected with 422 — "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 as null. 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 — warmup most consequentially. Re-send every boolean you want to keep, in the same call.
  • Filters are not validated. campaign/get's status accepts a value that is not a real status and answers 200 with 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).