Buyers API
Pause and resume a buyer from your own systems — endpoints, authentication, request bodies, and the full table of error codes.
Four endpoints let your own software stop and restart deliveries to a buyer. The usual reason: a charge fails on your side, and you want leads to stop going to that buyer within the second rather than the next time somebody opens the app.
Get a key first: API keys.
Authentication
Every request carries the key as a bearer token. There is no other scheme, and no cookie ever applies.
| Endpoint | Method | Scope |
|---|---|---|
/api/v1/buyers | GET | buyers:read |
/api/v1/buyers/{id} | GET | buyers:read |
/api/v1/buyers/{id}/pause | POST | buyers:write |
/api/v1/buyers/{id}/resume | POST | buyers:write |
{id} accepts a buyer's ID or its slug. The ID is on the buyer's page header, next to its name, with a copy button. The slug is what you see in the address bar (/buyers/apex-solar → apex-solar) — readable, and fine to hard-code, though it changes if the buyer is renamed.
The buyer object
Every endpoint answers with the same shape. It is identity plus availability — no pricing, no caps, no delivery configuration.
availability.receiving is the one field to branch on: true means this buyer is eligible for leads right now.
availability.state is active, paused, closed, disabled or archived. paused is indefinite; closed is a dated pause that expires by itself, and then carries closedUntil, reopenAt and behavior.
availability.pausedBy says who stopped it — admin (someone in your team, through the app), api (a key, through these endpoints), or buyer (the buyer paused itself from its portal). This is what tells you whether resume will do anything; see below.
reopenAt is when deliveries actually resume: the closing date composed with the buyer's operating hours. If a buyer is closed until 6:00 but opens at 9:00, closedUntil says 6:00 and reopenAt says 9:00. Use reopenAt. All timestamps are ISO 8601 in UTC.
Two things this object deliberately does not reflect: a buyer merely outside its operating hours reads active (it is active — it will receive when its window opens), and a buyer our delivery circuit breaker has paused after repeated endpoint failures also reads active, because that is our reaction to their server, not a decision anyone made about the buyer.
Pause a buyer
The body is optional. Every field in it is optional too.
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | string, ≤ 200 chars | — | Shown on the buyer's page and in the activity log |
until | ISO 8601, in the future | — | Pause until this instant, then reopen automatically |
behavior | "skip" | "hold" | "skip" | What happens to leads that arrive during the pause. Requires until |
Without until, the buyer is paused indefinitely — the same state as the Paused button in the app. It stops being selected immediately, and stays paused until something reopens it.
With until, you get a dated closure that expires on its own. behavior: "skip" (the default) sends arriving leads to your other buyers; "hold" parks them for this buyer until it reopens.
hold without until is refused (422 hold_requires_until). Holding leads with no end date consumes the buyer's caps and debits its prepaid balance on leads nobody will work.
changed is false when the call had no effect — most often because the buyer was already paused by someone else. That is not an error, and it is safe to retry: pausing is idempotent, and two simultaneous calls produce exactly one change and one activity-log entry.
Resume a buyer
Body optional; { "reason": "payment received" } is recorded in the activity log and stored nowhere.
Resume lifts only what the API paused. If availability.pausedBy is admin or buyer, the call returns 200 with changed: false and the buyer stays paused. This is not a limitation to work around — someone paused that buyer deliberately, possibly for quality or a contract dispute, and a billing system going green is not a reason to overrule them. Lift those from the buyer's page in the app.
Resuming also releases any leads that were held for this buyer, immediately rather than on the next sweep.
List and read
| Parameter | Meaning |
|---|---|
status | active, paused, disabled or archived. Archived buyers are excluded unless you ask for them |
q | Matches name, slug or email, case-insensitive |
limit | 1–200, default 50 |
cursor | The nextCursor from the previous page |
Page until nextCursor is null. It is a cursor and not an offset on purpose: buyers created while you paginate can't make you skip a row.
Errors
Errors always come back as JSON, never as a redirect or an HTML page:
The code is the contract — match on it, never on the message. Messages are free to improve.
| HTTP | code | When |
|---|---|---|
| 400 | validation_error | The body isn't valid JSON |
| 401 | unauthorized | No Authorization header, wrong scheme, or a key we don't recognise |
| 401 | key_revoked | The key exists but was revoked |
| 403 | insufficient_scope | The key lacks the scope this endpoint needs. Response carries required and granted |
| 404 | buyer_not_found | No buyer with that ID or slug in your organization |
| 405 | method_not_allowed | Wrong verb. The Allow header names the right one |
| 409 | buyer_not_pausable | The buyer is disabled or archived — it already receives nothing |
| 409 | buyer_not_resumable | The buyer is disabled or archived. Reactivate it in the app |
| 409 | setup_incomplete | Going live would route leads nowhere: the buyer has no active delivery method. Response carries missing: ["delivery"] |
| 409 | closure_conflict | A dated closure set by someone else is in force. Response carries existing |
| 422 | validation_error | A field is the wrong type, out of bounds, or until is in the past |
| 422 | hold_requires_until | behavior: "hold" with no until |
| 429 | rate_limited | Over 120 requests a minute for this key. Retry-After says how long to wait |
| 500 | internal_error | Our side. Safe to retry |
closure_conflict
You asked for until on a buyer already closed by an admin or by the buyer itself. Rather than moving a date somebody else chose, the call is refused and hands you theirs:
If you need the buyer stopped regardless, pause it without until: an indefinite pause is additive, never conflicts, and leaves their closure to expire on its own.
Worked example: stop on a failed charge, restart on payment
The same in shell, for a 7-day closure that expires by itself:
What shows up in the app
Nothing about an API pause is hidden.
The buyer's status control reads Paused · via API — go live (or Closed until Sep 6 · via API when you sent an until), and opening it names the key, the moment and the reason you sent:
The same · via API suffix appears on the buyer's badge in the buyers list. The buyer itself sees "Paused by your lead provider" on its portal and cannot lift it. The activity log records every change with the key as the actor — API key "Billing automation" — and the reason under it.
Revoking a key doesn't resume anything. Buyers it paused stay paused, and the status control then reads via API key (revoked) so nobody hunts for a key that is no longer in the list.
No email is sent for an API pause. It came from your own system, which already knows.
Not in this version
Outbound webhooks ("tell my system when a buyer is paused"), lead and pipeline endpoints, test-mode keys, and an OpenAPI document. Ask us if you need one — what gets built next is decided by who asks.