LeadMove Docs
Developers

Lead billing API

Validate leads for billing, mark them not billable, and read a buyer's billing statement from your own systems, for buyers billed after validation.

For buyers you bill after validation, your own systems can validate a delivered lead (it bills) or mark it not billable (it doesn't), and read a buyer's billing statement for a period. Use an organization key with the leads:bill scope.

POST https://app.leadmove.io/api/v1/leads/{leadId}/billing
GET  https://app.leadmove.io/api/v1/leads/{leadId}/billing
GET  https://app.leadmove.io/api/v1/buyers/{id}/billing

Your buyer can validate its own leads too, with its reporting key and accepted on the lead outcomes API. Both land in the same place.

Authentication

Create the key in Settings → Developers → Create key and tick leads:bill (API keys). Send it as a bearer token:

Authorization: Bearer lm_live_…
EndpointMethodScope
/api/v1/leads/{leadId}/billingPOST, GETleads:bill
/api/v1/buyers/{id}/billingGETleads:bill

{leadId} is the lead reference, LD-XXXXXX. It is on the lead page and in every webhook your buyers receive. {id} is a buyer's ID or slug, as on the buyers API.

Deciding a lead

curl -X POST https://app.leadmove.io/api/v1/leads/LD-7K2M4A/billing \
  -H "Authorization: Bearer lm_live_…" \
  -H "Content-Type: application/json" \
  -d '{"status":"not_billable","reason":"out of area"}'
FieldRequiredWhat it is
statusyesvalidated (the lead bills) or not_billable (it doesn't).
reasonnoWhy, with not_billable, up to 200 characters. Shown on the lead and in the statement.
buyeronly if the lead went to more than one buyer billed after validationThe buyer's ID or slug. Ignored otherwise.
{
  "data": {
    "leadId": "LD-7K2M4A",
    "buyer": { "id": "clx8f2a10000abcdef", "name": "Bobex" },
    "billing": {
      "status": "not_billable",
      "dueAt": null,
      "decidedAt": "2026-09-25T10:04:11.000Z",
      "source": "api",
      "reason": "out of area"
    }
  },
  "changed": true
}

Sending the same decision again returns changed: false and writes nothing. The API never reopens a decision: asking for the opposite of what was decided returns billing_already_decided. To undo one, reopen the lead in the app (invoice buyers only).

On a prepaid buyer, not_billable refunds the lead's price to the balance, once.

Reading a lead

GET /api/v1/leads/{leadId}/billing lists the lead's deliveries that carry a billing status:

{
  "data": {
    "leadId": "LD-7K2M4A",
    "deliveries": [
      {
        "buyer": { "id": "clx8f2a10000abcdef", "name": "Bobex" },
        "billing": {
          "status": "pending",
          "dueAt": "2026-10-01T10:12:00.000Z",
          "decidedAt": null,
          "source": null,
          "reason": null
        }
      }
    ]
  }
}

A lead delivered only to buyers billed on delivery answers "deliveries": [].

billing fieldWhat it is
statuspending (awaiting validation), validated or not_billable.
dueAtWhile pending: when the validation window ends.
decidedAtWhen it was decided.
sourceapi (an organization key), buyer_api (the buyer's reporting key), admin or bulk (in the app), expiry (the window ended).
reasonThe reason given with not_billable, if any.

A buyer's statement

The same statement as the buyer's Billing tab, for a period.

curl "https://app.leadmove.io/api/v1/buyers/bobex/billing?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer lm_live_…"
ParameterWhat it is
from, toCalendar days, YYYY-MM-DD, both included, in your organization's timezone. Up to 366 days. Omitted: this month.
statuspending, validated or not_billable: filters the rows. The totals always cover the whole period.
limitRows per page, 1 to 100. Default 50.
cursorThe nextCursor of the previous page.
{
  "data": {
    "buyer": { "id": "clx8f2a10000abcdef", "name": "Bobex" },
    "period": { "from": "2026-09-01", "to": "2026-09-30", "timezone": "America/Chicago" },
    "currency": "USD",
    "totals": {
      "validated": { "amount": "1260.00", "count": 18 },
      "pending": { "amount": "420.00", "count": 6, "dueToday": 2 },
      "notBillable": { "amount": "280.00", "count": 4 }
    },
    "rows": [
      {
        "leadId": "LD-8K2P1A",
        "deliveryId": "clx9a7b20000ghijkl",
        "buyerReference": "88213",
        "deliveredAt": "2026-09-22T14:05:00.000Z",
        "amount": "70.00",
        "currency": "USD",
        "billing": {
          "status": "not_billable",
          "dueAt": null,
          "decidedAt": "2026-09-23T10:00:00.000Z",
          "source": "admin",
          "reason": "duplicate"
        },
        "outcome": null,
        "disputeAccepted": false
      }
    ]
  },
  "nextCursor": "clx9a7b20000ghijkl"
}
  • Rows are newest delivery first. nextCursor is null on the last page.
  • validated counts what is billed: a validated lead refunded by an accepted dispute leaves the total, and its row says disputeAccepted: true.
  • outcome is what the buyer reported doing with the lead (in_progress, sold, lost), for reference. It never changes what is billed.
  • A buyer with nothing in the period answers empty rows, never an error.

Errors

Errors come back as JSON. The code is the contract; messages are free to improve.

{ "error": { "code": "buyer_required", "message": "…", "buyers": [{ "id": "…", "name": "Bobex" }] } }
HTTPcodeWhen
400validation_errorThe body is not valid JSON, or not an object
401unauthorizedNo Authorization header, wrong scheme, or a key we don't recognise
401key_revokedThe key was revoked
403insufficient_scopeThe key doesn't hold leads:bill
404lead_not_foundNo lead with that reference in your account
404buyer_not_foundNo buyer with that ID or slug in your account
405method_not_allowedWrong verb. The Allow header names the right one
409billing_already_decidedThe lead is already decided the other way. status and decidedAt in the error say how
422billing_not_enabledThe lead has no delivery to a buyer billed after validation (or none to the buyer you named)
422buyer_requiredThe lead went to more than one buyer billed after validation. buyers lists them; send one as buyer
422validation_errorA field or parameter is wrong: field names it
429rate_limitedToo many requests. Retry-After says how long to wait
500internal_errorOur side. Safe to retry

Debugging: every request is logged

Every request made with a recognised key is recorded, accepted or refused: the path, the body, the status and error code, and how long it took, along with the lead it was about. When a call does not behave as you expect, tell us when it was made and which lead it named: the answer is on record. Requests are kept 180 days.

Calls your buyer makes with its own reporting key show on the buyer's page, under Reporting API → Recent requests.

Common questions

Can a key validate leads for a buyer billed on delivery? No. Those leads bill at once and there is nothing to validate: the answer is billing_not_enabled.

What if nobody decides? When the buyer's validation window ends, the lead is validated or marked not billable automatically, as set on the buyer. The source then reads expiry.

Do the statement totals match the app? Yes. The Billing tab and this endpoint read the same computation, for the same period in the same timezone.

On this page