REST API

Served by payflow-backend. Read-only and unauthenticated — everything it returns is already public on-chain.

All amounts are strings in the token’s smallest unit (stroops for XLM). They are never JSON numbers, because JSON numbers are IEEE doubles and would lose precision on large i128 values.

Health

GET /health
{
  "status": "ok",
  "network": "testnet",
  "indexer": {
    "cursor": 5110940,
    "latestLedger": 5110941,
    "lag": 1,
    "lastRunAt": "2026-10-09T21:02:11.000Z",
    "lastError": null
  },
  "contracts": {
    "planRegistry": "CDQ5EJTVXXX2CBMHWP3BGGUDHSK6D4I25IF5XXH3QGL2TNTBMJUOFKDB",
    "vault": "CBNQEWSYYQOKNX5KB62OFT6ID5TCVSWN4PS4AXAJK6IWUEXOLVN6DXII",
    "subscription": "CD5YZUJU6WLFIHGAV4J2BV42GCVZE5ZGDUGSW5CBRIA47TQHSZZW3XMM"
  }
}

Plans

GET /api/plans?merchant=<G...>&limit=50
GET /api/plans/:id
{
  "plans": [
    {
      "id": 1,
      "merchant": "GB343X4K4TIYARSAAQZ55RIZY2YLFNOQCM3LNQSRBV56TMVGIF55WCOH",
      "token": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
      "amount": "10000000",
      "period": 60,
      "active": 1,
      "ledger": 4447697
    }
  ]
}

Mandates

GET /api/mandates?subscriber=<G...>&merchant=<G...>&status=Active
GET /api/mandates/:id
GET /api/mandates/:id/charges
{
  "mandates": [
    {
      "id": 1,
      "subscriber": "GAIJKXL3Q45ZR32QRDVPUDD4DPLT5GYCHJFESQPHHWJRYX4YZCG25JJR",
      "merchant": "GB343X4K4TIYARSAAQZ55RIZY2YLFNOQCM3LNQSRBV56TMVGIF55WCOH",
      "plan_id": 1,
      "amount": "10000000",
      "period": 60,
      "next_charge": 1788263112,
      "last_charge": 1788263052,
      "charges_made": 2,
      "max_charges": 0,
      "status": "Active"
    }
  ]
}

Charge history:

{
  "charges": [
    {
      "tx_hash": "ef182aaeb5d052833912bda4d38b282f1247da9ae5f1666937e811fa2f0d28ea",
      "mandate_id": 1,
      "amount": "9900000",
      "fee": "100000",
      "charges_made": 2,
      "ledger": 4447893
    }
  ]
}

amount is the merchant’s net share. Gross is amount + fee.

Due mandates

GET /api/due?limit=50

What the keeper works from. Reflects schedule and status; it does not verify the vault can cover the charge.

Summaries

GET /api/merchants/:address/summary
{
  "merchant": "GB343X4K4TIYARSAAQZ55RIZY2YLFNOQCM3LNQSRBV56TMVGIF55WCOH",
  "activeMandates": 1,
  "totalCollected": "19800000",
  "chargeCount": 2,
  "mrr": "432000000000"
}

mrr normalises every active mandate to a 30-day month. A short-period demo plan therefore yields a very large figure — that is arithmetic, not a bug.

GET /api/subscribers/:address/summary

totalPaid includes the protocol fee, since the subscriber paid it.

Stats

GET /api/stats
{
  "plans": 1,
  "mandates": 1,
  "activeMandates": 1,
  "charges": 2,
  "lastIndexedLedger": 4448232
}

lastIndexedLedger is the freshness signal. If it stops advancing, the indexer is stuck.

Errors

Status Body Meaning
404 {"error":"plan_not_found"} No such plan
404 {"error":"mandate_not_found"} No such mandate
404 {"error":"not_found"} Unknown route
500 {"error":"internal_error"} Unhandled failure

Treat this as a cache

The API serves a projection rebuilt from chain events. It can be deleted and rebuilt at any time. Never treat it as authoritative for money — read the contract for that. Frontend clients should degrade gracefully when it is unreachable, as src/lib/api.ts does by returning null rather than throwing.


This site uses Just the Docs, a documentation theme for Jekyll.