The API Experience

API documentation is a user experience problem.

Most API documentation is written from the inside out: here is what we built, here are its parameters. Developers read it from the outside in: here is what I am trying to do, show me the shortest safe path. My documentation is structured for the second reader.

That means every endpoint answers four questions in order: what does this do, what do I send, what comes back, and what happens when it fails. The last question is the one most documentation skips, and it is the one that costs the most support tickets. A developer who can diagnose a 401 from the docs alone never opens a ticket at all.

It also means the same fact frequently has to be written twice. A declined payment is one event. The developer branching on it needs a stable code and a retry rule; the merchant whose card was refused needs to know their money is safe and who to call. Writing one of those and handing it to the other is the most common failure in payments documentation, and it is usually the developer copy that leaks. Toggle the demo below to see the same decline in both registers.

The operation shown is a real one from the Ledger API, a specification I published in full as a demonstration piece, so the request, the response and the error bodies match the spec and the error reference exactly. The standards behind it are the ones I applied documenting 40+ REST endpoints and the Python SDK for a cloud workload automation platform, where the SAP and Docker integration paths turned out to be where the defects lived.

One declined payment, written for two readers.

One declined payment, written twice. The same event reaches a developer and a merchant, and they need opposite things from it.

POST/v1/paymentsLedger API · 2026-09-01
Request
POST /v1/payments
Authorization: Bearer sk_live_...
Idempotency-Key: 8f14e45f-ea1b-4c2d-9a35-7b1d6e0c3a92
Content-Type: application/json

{
  "invoice_id": "inv_7TgH2ReQvA",
  "amount": { "amount": 5520000, "currency": "INR" },
  "payment_method_id": "pm_4KdN8xWs1B"
}
Response · 201
HTTP/1.1 201 Created
Location: /v1/payments/pay_3MvB9kLd7Q

{
  "id": "pay_3MvB9kLd7Q",
  "object": "payment",
  "status": "failed",
  "invoice_id": "inv_7TgH2ReQvA",
  "amount": { "amount": 5520000, "currency": "INR" },
  "amount_refunded": { "amount": 0, "currency": "INR" },
  "amount_refund_pending": { "amount": 0, "currency": "INR" },
  "failure_code": "do_not_honor",
  "failure_message": "Issuer declined without a stated reason.",
  "created_at": "2026-09-01T14:32:07Z"
}

The card was declined and this is a 201. An HTTP status describes what happened to the request, not what happened commercially. Routing a decline through a 4xx sends it to a handler that cannot tell a refused card from a wrong API key, and into retry middleware that issuers read as abuse. Branch on status, never on the status code.

Error states · RFC 9457 problem+json

When documentation is right, developers don't need to ask questions.

← Back to Portfolio