Reference

REST API.

Read campaigns, customers, cancel sessions, and aggregate stats. JSON in, JSON out. Bearer-token auth. Rate-limited per token. The API is read-only today — the intended use case is custom dashboards and one-off scripts. For most workflows the dashboard or webhooks are easier.

Auth

Mint a token under Settings → API tokens— give it a name (e.g. “data warehouse”), click Create token, and copy the sk_live_… value. Each token is scoped to a single workspace and is shown onceat creation — we only store its hash. Lost tokens can't be recovered, only revoked + re-minted. Revoke from the same page; revoked tokens start returning 401 immediately.

curl https://www.trybackstop.com/api/v1/customers \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxx"

Send the token as Authorization: Bearer <token>. A missing header returns 401 authentication_required; a token that doesn't start with sk_live_, or one that's unknown or revoked, returns 401 invalid_token.

Endpoints

All endpoints are GET under /api/v1. List endpoints return up to 50 rows per page, newest-first.

  • GET /api/v1/customers — paginated list of mirrored customers. Filters: ?email= (exact match, case-insensitive), ?tag=, ?cursor=.
  • GET /api/v1/customers/:id — single customer (by Backstop UUID) with their subscriptions and recent_campaigns embedded.
  • GET /api/v1/campaigns — recovery campaigns. Filters: ?status=in_flight|recovered|lost|abandoned, ?cursor=.
  • GET /api/v1/campaigns/:id — single campaign with its embedded customer and subscription summary.
  • GET /api/v1/cancel_sessions — paginated list of cancel sessions. Filters: ?outcome= (one of saved_discount, saved_pause, saved_downgrade, saved_other, canceled, abandoned), ?cursor=<ISO timestamp>.
  • GET /api/v1/stats — aggregate counts and rates. Same numbers as the dashboard KPIs, in JSON.

A list response is a Stripe-style envelope. Each row carries an object tag (customer, recovery_campaign, cancel_session):

GET /api/v1/campaigns?status=in_flight

{
  "object": "list",
  "data": [
    {
      "id": "8f3c…",
      "object": "recovery_campaign",
      "stripe_invoice_id": "in_1A…",
      "amount_cents": 7900,
      "currency": "usd",
      "status": "in_flight",
      "decline_code": "insufficient_funds",
      "decline_category": "soft",
      "recovered_via": null,
      "first_failed_at": "2026-06-20T14:02:11Z",
      "recovered_at": null,
      "closed_at": null,
      "customer_id": "1c9a…",
      "subscription_id": "2e7b…"
    }
  ],
  "has_more": true,
  "next_cursor": "2026-06-20T14:02:11Z"
}

Monetary fields are integer minor units (amount_cents) — the stats endpoint also returns a display_currency and a has_multiple_currencies flag, since its totals are normalized across whatever currencies your campaigns ran in.

Pagination

Pagination is timestamp-cursor based, not offset based. Each list response includes next_cursor — an ISO 8601 timestamp of the last row — plus a boolean has_more. Pass next_cursor back as ?cursor= to fetch the next page. Rows come back newest-first, and the cursor bounds them to strictly before that timestamp.

Rate limits

Default limit is 60 requests per minute per token, measured over a sliding one-minute window. Need more? Hit reply on any Backstop email — we raise limits case-by-case rather than tiering.

Every response includes:

  • X-RateLimit-Limit — your per-minute ceiling (60)
  • X-RateLimit-Remaining — requests left in the current window
  • X-RateLimit-Reset — seconds remaining until the window refills

Hitting the limit returns HTTP 429 with a Retry-Afterheader (in seconds). Back off and retry; we don't auto-ban tokens for 429s.

Errors

All errors return JSON in the same nested shape (mirroring Stripe). Read error.type to branch programmatically and error.message for a human-readable reason:

{
  "error": {
    "type": "rate_limit_exceeded",
    "message": "60 requests per minute. Retry in 12s."
  }
}

error.type values you may see

  • authentication_required (401) — no Authorization header.
  • invalid_token (401) — token malformed, unknown, or revoked.
  • invalid_param (400) — a bad cursor, status, outcome, or non-UUID id.
  • not_found(404) — the requested customer or campaign isn't in your workspace.
  • rate_limit_exceeded (429) — see Rate limits above.
  • database_error (500) — on us; retry and, if it persists, hit reply.

On a 429 the wait time is also returned as a Retry-After response header (seconds) — there is no retry_after_seconds field in the body.

Want it broader?

We deliberately ship a small surface today. If you're missing an endpoint, hit reply on any Backstop email and tell us what you'd build with it — we add what actual use cases ask for, not theoretical completeness.

Related

  • Webhooks — push events instead of pulling.