# Ledger API documentation Spec-first documentation for the Ledger API, a billing API for invoices, payments, and refunds. The OpenAPI description is the source of truth. The generated reference, the error catalog, and the design record are written against it, and a governance ruleset stops the description from merging if it breaks a documented convention. The Ledger API is a portfolio piece. It is not a real product, and no endpoint described here is live. ## What is in this folder | File | Contents | |---|---| | `openapi.yaml` | OpenAPI 3.1.1 description: 11 operations, 7 webhooks, 20 component schemas | | `redocly.yaml` | Governance ruleset: 27 rules, 18 built-in rules raised to `error` and 9 written for this API | | `errors.md` | Error reference: 16 error codes, one cause and one fix each, plus the payment failure codes | | `design-decisions.md` | 11 design decisions, each with the alternative that was rejected | | `reference.html` | Reference generated from `openapi.yaml` by Redocly | | `package.json` | Scripts for linting and building | The API's release notes, with the full release history and the migration guide for release `2026-09-01`, are published separately under [Ledger API release notes](/samples/ledger-releases). ## Running the checks Requires Node.js 20.19 or later. ```bash npm install npm run lint # check openapi.yaml against the ruleset npm run build # regenerate reference.html npm run check # both, in that order ``` In CI, `npm run lint` runs on every pull request that touches the description. Make the job a required check, and a failing lint blocks the merge: ```yaml name: api-description on: pull_request: paths: ["openapi.yaml", "redocly.yaml"] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm run lint ``` ## The ruleset `redocly.yaml` extends Redocly's `recommended-strict` configuration, raises 18 built-in rules to `error`, and adds 9 custom rules. The custom rules encode the conventions this API depends on: | Rule | What it blocks | |---|---| | `rule/no-floating-point-money` | `type: number` on any monetary field | | `rule/every-operation-documents-401` | An operation with no `401` response | | `rule/every-operation-documents-429` | An operation with no `429` response | | `rule/every-operation-documents-500` | An operation with no `500` response | | `rule/operationid-is-camel-case` | An `operationId` that is not camelCase, since it becomes an SDK method name | | `rule/schema-names-are-pascal-case` | A component schema name that is not PascalCase | | `rule/summary-has-no-trailing-period` | An operation summary written as a sentence | | `rule/summary-is-not-a-sentence` | An operation summary longer than 60 characters | | `rule/named-schemas-are-defined` | A component schema entry with no definition | ### How the ruleset was tested A ruleset that never fails proves nothing, so it was run against copies of `openapi.yaml` with defects introduced on purpose. Each defect was reported by the rule expected to catch it: | Defect introduced | Reported by | |---|---| | `type: number` on `Money.amount` | `rule/no-floating-point-money` | | `401` removed from `getRefund` | `rule/every-operation-documents-401` | | `operationId: Delete_Invoice` | `rule/operationid-is-camel-case` | | Description removed from the `refund_id` parameter | `parameter-description` | An earlier version of this table credited the fourth defect to a custom rule, `rule/named-schemas-have-descriptions`. Running the test showed that the rule only checks that schemas exist, and that the built-in `parameter-description` is what catches the defect. The table was corrected, and the rule was renamed to `rule/named-schemas-are-defined` so that its name no longer claims a check it does not make. ## The API at a glance ``` GET /v1/invoices List invoices POST /v1/invoices Create a draft invoice GET /v1/invoices/{invoice_id} Retrieve an invoice PATCH /v1/invoices/{invoice_id} Update a draft invoice DELETE /v1/invoices/{invoice_id} Delete a draft invoice POST /v1/invoices/{invoice_id}/finalize Finalize a draft invoice POST /v1/invoices/{invoice_id}/void Void an open invoice POST /v1/payments Create a payment against an invoice GET /v1/payments/{payment_id} Retrieve a payment POST /v1/payments/{payment_id}/refunds Refund a payment GET /v1/refunds/{refund_id} Retrieve a refund ``` Webhooks: `invoice.finalized`, `invoice.paid`, `invoice.voided`, `payment.succeeded`, `payment.failed`, `refund.succeeded`, `refund.failed`. ## Three decisions worth arguing with The full reasoning is in `design-decisions.md`. These three are the ones integrators push back on most. **A declined payment returns `201`, not `402`.** The status code describes the fate of the request, not the outcome of the payment. A decline is a request that was understood and attempted, so it returns a `Payment` with `status: failed` and a `failure_code`: ```json { "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": "card_expired", "failure_message": "The card's expiry date has passed.", "created_at": "2026-09-01T14:32:07Z" } ``` Generic HTTP middleware treats a `4xx` as an error to retry or surface, and a retried decline is something issuers treat as abuse. **Money is an integer in the currency's minor unit.** Binary floating point cannot represent most decimal fractions exactly: ```js 0.1 + 0.2; // 0.30000000000000004 10 + 20; // 30, the same sum in paise ``` The error is invisible on one invoice and material across a month of reconciliation. `{ "amount": 10050, "currency": "USD" }` is $100.50, with no rounding anywhere. **A resource in another account returns `404`, not `403`.** A `403` would confirm that the identifier exists, which is enough to enumerate the identifier space. The ambiguity is deliberate and documented. ## Standards | Standard | Used for | |---|---| | OpenAPI 3.1.1 | The API description | | RFC 9457 | Error bodies (`application/problem+json`) | | RFC 9110 | HTTP semantics, including `Retry-After` | | RFC 6901 | JSON Pointer, for `errors[].field` | | RFC 3339 | Timestamps | | `draft-ietf-httpapi-ratelimit-headers` | `RateLimit` and `RateLimit-Policy`; an Internet-Draft, revision 11, May 2026 | ## Author Murali Krishna Kolipaka, Senior Technical Writer, Hyderabad, India. [muralikrishna0293@gmail.com](mailto:muralikrishna0293@gmail.com) ยท [LinkedIn](https://linkedin.com/in/murali-krishna66) Released under the MIT License.