LeadMove Docs
Developers

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.

POST https://app.leadmove.io/api/v1/buyers/{id}/pause
POST https://app.leadmove.io/api/v1/buyers/{id}/resume
GET  https://app.leadmove.io/api/v1/buyers/{id}
GET  https://app.leadmove.io/api/v1/buyers

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.

Authorization: Bearer lm_live_…
EndpointMethodScope
/api/v1/buyersGETbuyers:read
/api/v1/buyers/{id}GETbuyers:read
/api/v1/buyers/{id}/pausePOSTbuyers:write
/api/v1/buyers/{id}/resumePOSTbuyers: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-solarapex-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.

{
  "id": "clx8f2a10000abcdef",
  "slug": "apex-solar",
  "name": "Apex Solar Co.",
  "email": "ops@apex.example",
  "status": "paused",
  "availability": {
    "receiving": false,
    "state": "paused",
    "pausedBy": "api",
    "reason": "charge failed",
    "since": "2026-08-30T14:02:11.000Z",
    "closedUntil": null,
    "reopenAt": null,
    "behavior": null
  },
  "createdAt": "2026-01-05T09:30:00.000Z",
  "updatedAt": "2026-08-30T14:02:11.000Z"
}

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

POST /api/v1/buyers/{id}/pause

The body is optional. Every field in it is optional too.

FieldTypeDefaultMeaning
reasonstring, ≤ 200 charsShown on the buyer's page and in the activity log
untilISO 8601, in the futurePause 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.

curl -X POST https://app.leadmove.io/api/v1/buyers/apex-solar/pause \
  -H "Authorization: Bearer $LEADMOVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "charge failed"}'
{ "data": { "...": "the buyer object" }, "changed": true }

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

POST /api/v1/buyers/{id}/resume

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.

curl -X POST https://app.leadmove.io/api/v1/buyers/apex-solar/resume \
  -H "Authorization: Bearer $LEADMOVE_API_KEY"

Resuming also releases any leads that were held for this buyer, immediately rather than on the next sweep.

List and read

GET /api/v1/buyers?status=active&q=solar&limit=50&cursor=…
GET /api/v1/buyers/{id}
ParameterMeaning
statusactive, paused, disabled or archived. Archived buyers are excluded unless you ask for them
qMatches name, slug or email, case-insensitive
limit1–200, default 50
cursorThe nextCursor from the previous page
{ "data": [ { "...": "buyer" } ], "nextCursor": "clx8f2a10000abcdef" }

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:

{ "error": { "code": "closure_conflict", "message": "…" } }

The code is the contract — match on it, never on the message. Messages are free to improve.

HTTPcodeWhen
400validation_errorThe body isn't valid JSON
401unauthorizedNo Authorization header, wrong scheme, or a key we don't recognise
401key_revokedThe key exists but was revoked
403insufficient_scopeThe key lacks the scope this endpoint needs. Response carries required and granted
404buyer_not_foundNo buyer with that ID or slug in your organization
405method_not_allowedWrong verb. The Allow header names the right one
409buyer_not_pausableThe buyer is disabled or archived — it already receives nothing
409buyer_not_resumableThe buyer is disabled or archived. Reactivate it in the app
409setup_incompleteGoing live would route leads nowhere: the buyer has no active delivery method. Response carries missing: ["delivery"]
409closure_conflictA dated closure set by someone else is in force. Response carries existing
422validation_errorA field is the wrong type, out of bounds, or until is in the past
422hold_requires_untilbehavior: "hold" with no until
429rate_limitedOver 120 requests a minute for this key. Retry-After says how long to wait
500internal_errorOur 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:

{
  "error": {
    "code": "closure_conflict",
    "message": "…",
    "existing": {
      "closedUntil": "2026-09-06T12:00:00.000Z",
      "reopenAt": "2026-09-06T14:00:00.000Z",
      "behavior": "skip",
      "pausedBy": "admin"
    }
  }
}

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

const BASE = 'https://app.leadmove.io/api/v1';
const headers = {
  Authorization: `Bearer ${process.env.LEADMOVE_API_KEY}`,
  'Content-Type': 'application/json',
};
 
async function onChargeFailed(buyerSlug) {
  const res = await fetch(`${BASE}/buyers/${buyerSlug}/pause`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ reason: 'charge failed' }),
  });
  const body = await res.json();
 
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  if (!body.changed) {
    // Already paused — by us on an earlier retry, or by a human.
    console.log(`already paused by ${body.data.availability.pausedBy}`);
  }
}
 
async function onPaymentReceived(buyerSlug) {
  const res = await fetch(`${BASE}/buyers/${buyerSlug}/resume`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ reason: 'payment received' }),
  });
  const body = await res.json();
 
  if (res.status === 409 && body.error.code === 'setup_incomplete') {
    // The buyer lost its delivery method while paused. A human has to look.
    return alertOps(buyerSlug, body.error.message);
  }
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
 
  if (!body.changed && !body.data.availability.receiving) {
    // Someone in your team paused this buyer too. We don't override that.
    alertOps(buyerSlug, `still paused by ${body.data.availability.pausedBy}`);
  }
}

The same in shell, for a 7-day closure that expires by itself:

curl -X POST https://app.leadmove.io/api/v1/buyers/apex-solar/pause \
  -H "Authorization: Bearer $LEADMOVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"until": "2026-09-06T12:00:00Z", "behavior": "skip", "reason": "invoice overdue"}'

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:

Buyer status
┌──────────────────────────────────────────────────┐
│ Paused via API key "Billing automation" · 2 h ago│
│ "charge failed"                                  │
│ The buyer can't resume this from the portal.     │
└──────────────────────────────────────────────────┘
  Active        Receives leads
  Paused · current

  Automate
  Copy pause request
  Copy resume request

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.

On this page