42  API Design Without Hidden Compatibility Traps

43 API Design Without Hidden Compatibility Traps

Expected delta: The repaired spell forces the design to expose compatibility, authorization, idempotency, and migration surfaces up front.

Fixture: examples/evaluations/fixtures/api-design

Observed outcome delta: weak and repaired prompts tied on outcome checks

Observed reviewability delta: repaired prompts scored 2.9 reviewability points higher on average

Input context: The fixture describes a public billing API that must remain compatible with slow-upgrading clients.

Files:

  • requirements.json: product and compatibility requirements.
  • client_contracts.json: existing client assumptions.
  • ground_truth.json: planted compatibility traps.

Ground truth:

  • Payment attempts need idempotency keys.
  • Error responses need stable machine-readable codes.
  • List endpoints need pagination.
  • Mobile clients require backward-compatible versioning.
  • Refunds and invoices are separate resources with linked lifecycle state.

43.1 Surface and Tier Delta Summary

Surface Tier Weak n Repaired n Weak Reviewability Repaired Reviewability Reviewability Delta Weak Outcome Repaired Outcome Outcome Delta
claude-code-safe clean 1 1 7.0 9.0 2.0 3.0 3.0 0.0
claude-code-safe trap 1 1 5.0 8.0 3.0 3.0 3.0 0.0
codex-cli-default clean 3 3 7.0 10.3 3.3 3.0 3.0 0.0
codex-cli-default trap 3 3 7.0 9.7 2.7 3.0 3.0 0.0

43.2 Scores

Surface Tier Variant Rep Artifact Invariant Output Verify Failure Assumptions Reviewability Outcome
codex-cli-default clean weak 1 2 2 2 0 1 1 8 3
codex-cli-default clean repaired 1 2 1 2 1 2 2 10 3
codex-cli-default clean weak 2 2 1 2 0 1 1 7 3
codex-cli-default clean repaired 2 2 1 2 1 2 2 10 3
codex-cli-default clean weak 3 2 1 2 0 1 0 6 3
codex-cli-default clean repaired 3 2 2 2 2 2 1 11 3
claude-code-safe clean weak 1 2 1 2 0 1 1 7 3
claude-code-safe clean repaired 1 2 0 2 1 2 2 9 3
claude-code-safe trap weak 1 2 0 2 0 1 0 5 3
claude-code-safe trap repaired 1 2 0 2 1 2 1 8 3
codex-cli-default trap weak 1 2 2 2 0 1 1 8 3
codex-cli-default trap repaired 1 2 1 2 0 2 2 9 3
codex-cli-default trap weak 2 1 1 2 0 1 1 6 3
codex-cli-default trap repaired 2 2 1 2 0 2 2 9 3
codex-cli-default trap weak 3 2 1 2 0 1 1 7 3
codex-cli-default trap repaired 3 2 1 2 2 2 2 11 3

43.3 Transcripts

43.3.1 codex-cli-default - weak r1

The fixture is missing exact lifecycle status names, amount/currency rules, and payment processor details, so those should be filled from production contracts. This design preserves compatibility.

| Endpoint | Method | Scope | Notes |
|---|---:|---|---|
| `/v1/invoices` | `GET` | `billing:read` | Cursor-paginated list |
| `/v1/invoices/{invoice_id}` | `GET` | `billing:read` | Stable invoice status values; never rename |
| `/v1/payment_attempts` | `POST` | `billing:write` | Requires `Idempotency-Key` header |
| `/v1/payment_attempts/{attempt_id}` | `GET` | `billing:read` | Retry-safe result lookup |
| `/v1/refunds` | `POST` | `billing:write` | Separate refund resource linked to invoice/payment |
| `/v1/refunds/{refund_id}` | `GET` | `billing:read` | Linked lifecycle state |
| `/v1/account_credits` | `GET` | `billing:read` | Cursor-paginated list |

**Payment Attempt Request**

```http
POST /v1/payment_attempts
Authorization: Bearer <token>
Idempotency-Key: pay_2026_07_01_customer_123_invoice_456
Content-Type: application/json
{
  "invoice_id": "inv_456",
  "account_id": "acct_123",
  "amount": 5000
}

Payment Attempt Response

{
  "id": "pa_789",
  "invoice_id": "inv_456",
  "status": "<stable_payment_attempt_status>",
  "created_at": "2026-07-01T12:00:00Z"
}

If the same idempotency key is retried with the same body, return the original result. If reused with a different body, return a stable error.

Paginated List Response

GET /v1/invoices?limit=50&cursor=cur_abc
{
  "data": [
    {
      "id": "inv_456",
      "status": "<existing_invoice_status>",
      "refund_ids": ["ref_123"]
    }
  ],
  "pagination": {
    "next_cursor": "cur_def",
    "has_more": true
  }
}

Refund Response

{
  "id": "ref_123",
  "invoice_id": "inv_456",
  "payment_attempt_id": "pa_789",
  "status": "<stable_refund_status>"
}

Stable Error Schema

{
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY",
    "message": "The idempotency key was already used for a different request.",
    "request_id": "req_abc"
  }
}

Compatibility rules: keep /v1 backward compatible, add fields only, never rename invoice statuses or error fields, and support old mobile behavior for at least the 120-day upgrade window.


### codex-cli-default - repaired r1

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T04:38:04.444991+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/r01-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r01-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/r01-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r01-repaired-output.md)
- Reviewability total: 10
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
Assumptions: amounts are integer minor units plus currency; invoice status values are stable enums; refunds are separate resources linked to invoices and successful payment attempts.

| Method | Endpoint | Scope | Notes |
|---|---|---|---|
| `GET` | `/v1/invoices?limit=50&cursor=...` | `billing:read` | Paginated list |
| `POST` | `/v1/invoices` | `billing:write` | Create invoice |
| `GET` | `/v1/invoices/{invoice_id}` | `billing:read` | Stable invoice shape |
| `GET` | `/v1/invoices/{invoice_id}/payment_attempts` | `billing:read` | Paginated attempts |
| `POST` | `/v1/invoices/{invoice_id}/payment_attempts` | `billing:write` | Requires `Idempotency-Key` |
| `GET` | `/v1/payment_attempts/{attempt_id}` | `billing:read` | Retry/status polling |
| `POST` | `/v1/payment_attempts/{attempt_id}/refunds` | `billing:write` | Requires `Idempotency-Key` |
| `GET` | `/v1/refunds?invoice_id=...&limit=50&cursor=...` | `billing:read` | Paginated refunds |
| `GET` | `/v1/account_credits?limit=50&cursor=...` | `billing:read` | Existing resource support |

Create payment attempt:

```http
POST /v1/invoices/inv_123/payment_attempts
Authorization: Bearer <token>
Idempotency-Key: 7f6b2f0e-...
Content-Type: application/json
{
  "amount": 1299,
  "currency": "USD",
  "payment_method_id": "pm_123"
}
{
  "id": "pa_123",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount": 1299,
  "currency": "USD",
  "created_at": "2026-07-01T12:00:00Z"
}

Paginated list response:

{
  "data": [{ "id": "inv_123", "status": "paid", "refund_state": "none" }],
  "pagination": { "next_cursor": "cur_abc", "has_more": true }
}

Refund response:

{
  "id": "rf_123",
  "invoice_id": "inv_123",
  "payment_attempt_id": "pa_123",
  "status": "pending",
  "amount": 500
}

Stable error schema:

{
  "error": {
    "code": "idempotency_key_required",
    "message": "Idempotency-Key is required.",
    "request_id": "req_123"
  }
}

Auth: OAuth bearer tokens only. Preserve existing scopes: billing:read for reads, billing:write for mutations.

Idempotency: all payment attempts and refunds require Idempotency-Key. Reusing a key with the same body returns the original result; reusing it with a different body returns 409 with code idempotency_key_conflict.

Versioning/migration: keep /v1; additive fields only; never rename status values or change error shape. Mobile clients have a 120-day upgrade window, so breaking changes require /v2 and overlap.

Verification risks: race concurrent retries with a unique idempotency-key constraint; prevent double refund by checking refundable balance transactionally; do not rely on clients preserving unknown fields.


### codex-cli-default - weak r2

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T04:38:46.757444+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/r02-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r02-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/r02-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r02-weak-output.md)
- Reviewability total: 7
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
Missing specifics: amount/currency format, full lifecycle enums, refund eligibility rules, and account-credit behavior. This design keeps those as stable fields without inventing business rules.

**API Shape**

Base path: `/v1/billing`
Auth: OAuth bearer token. Existing scopes: `billing:read`, `billing:write`.

| Method | Endpoint | Scope | Notes |
|---|---|---:|---|
| `GET` | `/invoices?limit=50&page_token=...` | `billing:read` | Paginated invoice list |
| `GET` | `/invoices/{invoice_id}` | `billing:read` | Invoice detail |
| `GET` | `/payment_attempts?invoice_id=...&limit=50&page_token=...` | `billing:read` | Paginated attempts |
| `POST` | `/payment_attempts` | `billing:write` | Requires `Idempotency-Key` |
| `GET` | `/refunds?invoice_id=...&limit=50&page_token=...` | `billing:read` | Refunds are separate resources |
| `POST` | `/refunds` | `billing:write` | Creates refund linked to invoice/payment attempt |
| `GET` | `/account_credits?limit=50&page_token=...` | `billing:read` | Paginated credits |

**Payment Attempt**

Request:

```http
POST /v1/billing/payment_attempts
Authorization: Bearer <token>
Idempotency-Key: pay_2026_07_01_customer_123_invoice_456
Content-Type: application/json
{
  "invoice_id": "inv_456",
  "payment_method_id": "pm_789"
}

Response:

{
  "id": "pa_123",
  "invoice_id": "inv_456",
  "status": "processing",
  "created_at": "2026-07-01T00:00:00Z"
}

Repeated requests with the same Idempotency-Key must return the original result, not create a second charge.

Invoice With Linked Lifecycle

{
  "id": "inv_456",
  "status": "open",
  "payment_attempt_ids": ["pa_123"],
  "refund_ids": ["ref_222"],
  "account_credit_ids": []
}

Refunds remain separate resources:

{
  "id": "ref_222",
  "invoice_id": "inv_456",
  "payment_attempt_id": "pa_123",
  "status": "pending"
}

Pagination

List responses use a stable envelope:

{
  "data": [{ "id": "inv_456", "status": "open" }],
  "next_page_token": "opaque_token_or_null"
}

Stable Error Schema

Never change this shape without a new version:

{
  "error": {
    "code": "idempotency_key_required",
    "message": "Idempotency-Key header is required.",
    "request_id": "req_123"
  }
}

code is the stable machine-readable contract.

Compatibility Notes

Use /v1 versioning. Do not rename invoice status values, change the error shape, or make payment retries non-idempotent. Changes should be additive because mobile clients may take 120 days to upgrade.


### codex-cli-default - repaired r2

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T04:39:30.761061+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/r02-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r02-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/r02-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r02-repaired-output.md)
- Reviewability total: 10
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
**Assumptions / Missing**
Amounts, currencies, exact status enum values, customer model, payment provider behavior, invoice line items, and account credit lifecycle are not specified. Design below uses generic fields and treats refunds/invoices as separate linked resources.

**Endpoint Table**

| Method | Endpoint | Scope | Notes |
|---|---|---:|---|
| `GET` | `/v1/invoices?limit=&cursor=&status=` | `billing:read` | Paginated list |
| `GET` | `/v1/invoices/{invoice_id}` | `billing:read` | Stable invoice representation |
| `POST` | `/v1/invoices` | `billing:write` | Create invoice |
| `GET` | `/v1/invoices/{invoice_id}/payment_attempts?limit=&cursor=` | `billing:read` | Paginated attempts |
| `POST` | `/v1/invoices/{invoice_id}/payment_attempts` | `billing:write` | Requires `Idempotency-Key` |
| `GET` | `/v1/payment_attempts/{attempt_id}` | `billing:read` | Fetch retry result |
| `GET` | `/v1/refunds?invoice_id=&limit=&cursor=` | `billing:read` | Paginated refunds |
| `POST` | `/v1/refunds` | `billing:write` | Separate refund resource linked to invoice/payment attempt |
| `GET` | `/v1/account_credits?limit=&cursor=` | `billing:read` | Included because fixture lists resource |

**Examples**

`POST /v1/invoices`

```json
{
  "customer_id": "cus_123",
  "amount_minor": 5000,
  "currency": "USD"
}
{
  "id": "inv_123",
  "status": "open",
  "amount_minor": 5000,
  "currency": "USD",
  "created_at": "2026-07-01T12:00:00Z"
}

POST /v1/invoices/inv_123/payment_attempts with Idempotency-Key: pay_20260701_inv_123_001

{
  "payment_method_id": "pm_123"
}
{
  "id": "pa_123",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount_minor": 5000
}

Paginated list response:

{
  "data": [{ "id": "inv_123", "status": "open" }],
  "page": { "limit": 50, "next_cursor": "cur_abc" }
}

Stable error:

{
  "error": {
    "code": "idempotency_key_required",
    "message": "Idempotency-Key header is required.",
    "request_id": "req_123"
  }
}

Auth Use OAuth bearer tokens. Existing scopes remain billing:read and billing:write; do not rename them without a new API version.

Idempotency All payment attempts require Idempotency-Key. Same key plus same request returns the original result. Same key with different body returns 409 and stable code idempotency_key_conflict.

Versioning / Migration Use /v1. Add fields only; do not rename invoice statuses, scopes, or error shape. Support old mobile clients for at least 120 days. New breaking behavior requires /v2.

Verification Risks Race conditions: duplicate payment attempts for one invoice, refund while payment is pending, concurrent refund requests. Use PostgreSQL transactions, unique idempotency keys, and state transition checks.

Retry hazards: client timeout after successful charge; idempotency must return the existing attempt.

Compatibility risks: enum renames, unpaginated lists, changed error schema, and coupling refunds directly into invoice objects.


### codex-cli-default - weak r3

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T04:40:14.774879+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/r03-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r03-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/r03-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r03-weak-output.md)
- Reviewability total: 6
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
**Billing API Design**

Base path: `/v1/billing`
Versioning: keep `/v1` backward compatible. Only additive response fields are allowed. Do not rename status values or change error shape without a new version. Mobile-breaking removals require at least the 120-day mobile upgrade window.

| Method | Endpoint | Scope | Purpose |
|---|---|---:|---|
| `GET` | `/invoices` | `billing:read` | List invoices, paginated |
| `GET` | `/invoices/{invoice_id}` | `billing:read` | Fetch invoice |
| `POST` | `/payment_attempts` | `billing:write` | Attempt payment, idempotent |
| `GET` | `/payment_attempts` | `billing:read` | List attempts, paginated |
| `GET` | `/payment_attempts/{attempt_id}` | `billing:read` | Fetch attempt |
| `POST` | `/refunds` | `billing:write` | Create refund as separate resource |
| `GET` | `/refunds` | `billing:read` | List refunds, paginated |
| `GET` | `/refunds/{refund_id}` | `billing:read` | Fetch refund |
| `GET` | `/account_credits` | `billing:read` | List account credits, paginated |

Payment attempts require an `Idempotency-Key` header. Repeating the same request with the same key returns the same payment attempt. Reusing the key with a different body returns `409 idempotency_key_conflict`.

Example:

```http
POST /v1/billing/payment_attempts
Authorization: Bearer ...
Idempotency-Key: pay_2026_001
Content-Type: application/json
{
  "invoice_id": "inv_123",
  "payment_method_id": "pm_456",
  "amount": 5000,
  "currency": "USD"
}

Response:

{
  "id": "pa_789",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount": 5000,
  "currency": "USD",
  "created_at": "2026-07-01T12:00:00Z"
}

Refunds are separate from invoices but linked:

{
  "id": "ref_123",
  "invoice_id": "inv_123",
  "payment_attempt_id": "pa_789",
  "status": "pending",
  "amount": 2000,
  "currency": "USD"
}

List responses use cursor pagination:

GET /v1/billing/invoices?limit=50&page_after=cursor_abc
{
  "data": [],
  "next_page_token": "cursor_def"
}

Stable error schema:

{
  "error": {
    "code": "idempotency_key_required",
    "message": "Idempotency-Key header is required.",
    "request_id": "req_123",
    "details": {}
  }
}

Error code is the machine contract; messages may change, codes must not.


### codex-cli-default - repaired r3

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T04:40:46.554091+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/r03-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r03-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/r03-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/r03-repaired-output.md)
- Reviewability total: 11
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
**Assumptions:** amounts are integer minor units plus ISO currency; invoices, payment attempts, and refunds are separate resources; refunds link to a successful payment attempt and update invoice/payment lifecycle state.

| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/v1/invoices?customer_id=&limit=&cursor=` | List invoices with cursor pagination |
| `GET` | `/v1/invoices/{id}` | Fetch invoice |
| `POST` | `/v1/invoices` | Create invoice |
| `GET` | `/v1/payment_attempts?invoice_id=&limit=&cursor=` | List attempts |
| `POST` | `/v1/payment_attempts` | Create/retry payment attempt |
| `GET` | `/v1/payment_attempts/{id}` | Fetch attempt |
| `GET` | `/v1/refunds?invoice_id=&limit=&cursor=` | List refunds |
| `POST` | `/v1/refunds` | Create refund |
| `GET` | `/v1/refunds/{id}` | Fetch refund |

**Create Payment Attempt**

```http
POST /v1/payment_attempts
Authorization: Bearer <oauth-token>
Idempotency-Key: payatt_2026_07_01_abc123
Content-Type: application/json
{
  "invoice_id": "inv_123",
  "amount": 2599,
  "currency": "USD",
  "payment_method_id": "pm_456"
}
{
  "id": "pa_789",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount": 2599,
  "currency": "USD",
  "created_at": "2026-07-01T12:00:00Z"
}

Paginated List

{
  "data": [
    { "id": "inv_123", "status": "paid", "amount_due": 2599, "currency": "USD" }
  ],
  "pagination": {
    "limit": 50,
    "next_cursor": "cur_abc",
    "has_more": true
  }
}

Error Schema

{
  "error": {
    "code": "invoice_already_paid",
    "message": "Invoice is already paid.",
    "request_id": "req_123",
    "details": {
      "invoice_id": "inv_123"
    }
  }
}

Error code is stable and machine-readable. Messages may change.

Auth Notes

Use OAuth bearer tokens. Scope by action: billing.invoices.read, billing.invoices.write, billing.payments.write, billing.refunds.write. Never infer tenant/customer access from request body alone; validate ownership server-side.

Idempotency Notes

Require Idempotency-Key for all mutating endpoints. Store key, endpoint, OAuth subject, request hash, response body, status code, and expiry. Same key plus same request returns the original response. Same key plus different body returns idempotency_key_conflict.

Migration / Versioning Notes

Use /v1 path versioning. Preserve response fields and enum meanings. Additive fields are allowed. Avoid removing fields, changing types, or reusing error codes. Mobile clients may lag, so support old versions through a documented deprecation window.

Verification Risks

Race conditions: concurrent payment attempts for one invoice, duplicate refunds, refund amount exceeding captured amount, invoice status changing during payment. Use DB constraints, transactions, row locks, and lifecycle checks.

Retry hazards: network timeout after charge succeeds; client retries must receive the same attempt, not create a second charge.

Compatibility risks: enum additions, pagination shape changes, unstable errors, and merging refunds into invoices would break slow-upgrading clients.


### claude-code-safe - weak r1

- Surface label: Claude Code CLI with tools disabled
- Run timestamp: `2026-07-02T08:30:18.744520+00:00`
- Prompt file: [examples/evaluations/runs/claude-code-safe/api-design/clean/r01-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/clean/r01-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/claude-code-safe/api-design/clean/r01-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/clean/r01-weak-output.md)
- Reviewability total: 7
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
## Billing API Design

### Versioning

All endpoints are versioned via URL prefix: `/v1/`. Breaking changes increment the major version. Mobile clients on older versions continue to receive responses in their requested schema via `Accept: application/vnd.billing.v1+json`.

---

### Payments

POST /v1/payments Headers: Idempotency-Key: # required; server rejects duplicates within 24h Body: { amount, currency, source, metadata } Response: { id, status, created_at }


Idempotency keys are **required** — the server returns `400 MISSING_IDEMPOTENCY_KEY` if absent. Replaying the same key within the TTL returns the original response without side effects.

---

### Invoices

POST /v1/invoices GET /v1/invoices/{id} GET /v1/invoices?cursor=&limit= # paginated PATCH /v1/invoices/{id} # draft updates only


Invoice lifecycle states: `draft → open → paid | void`. The `status` field is stable and machine-readable.

---

### Refunds

POST /v1/refunds Headers: Idempotency-Key: # required Body: { payment_id, amount, reason }

GET /v1/refunds/{id} GET /v1/refunds?cursor=&limit=


Refunds are a **separate resource** linked to a payment, not a sub-action of `Invoice`. A refund transitions through `pending → succeeded | failed`. When a refund succeeds, the linked invoice transitions to `refunded` — this is the lifecycle link between the two resources.

---

### Pagination

All list endpoints use cursor-based pagination:

```json
{
  "data": [...],
  "next_cursor": "opaque_token",
  "has_more": true
}

No offset pagination — it breaks under concurrent writes and is incompatible with the slow-upgrading client guarantee.


43.3.2 Error Responses

Every error returns a stable, machine-readable code:

{
  "error": {
    "code": "CARD_DECLINED",
    "message": "Human-readable detail.",
    "request_id": "req_abc123"
  }
}

message may change; code is a stable contract. Error codes are documented and semver-protected. Example codes: MISSING_IDEMPOTENCY_KEY, INVALID_AMOUNT, INVOICE_NOT_FOUND, REFUND_EXCEEDS_ORIGINAL.


43.3.3 Compatibility Contract

Concern Mechanism
Slow-upgrading mobile clients URL versioning + additive-only field changes
Duplicate payment prevention Required Idempotency-Key header
Stable error handling Machine-readable code field, never removed
Large result sets Cursor pagination on all list endpoints
Refund/invoice coupling Separate resources with shared status transitions

### claude-code-safe - repaired r1

- Surface label: Claude Code CLI with tools disabled
- Run timestamp: `2026-07-02T08:30:37.725893+00:00`
- Prompt file: [examples/evaluations/runs/claude-code-safe/api-design/clean/r01-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/clean/r01-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/claude-code-safe/api-design/clean/r01-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/clean/r01-repaired-output.md)
- Reviewability total: 9
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
## Billing API Design

**Assumptions (resource model is fully specified by ground truth):**
- Refunds reference a Payment; Invoices reference a Payment when settled
- Invoice lifecycle: `draft → open → paid | void`
- Payment lifecycle: `pending → succeeded | failed`
- Refund lifecycle: `pending → succeeded | failed`

---

### Endpoint Table

| Method | Path | Description |
|--------|------|-------------|
| POST | `/v1/invoices` | Create invoice |
| GET | `/v1/invoices` | List invoices (paginated) |
| GET | `/v1/invoices/{id}` | Get invoice |
| POST | `/v1/invoices/{id}/payments` | Attempt payment on invoice |
| GET | `/v1/invoices/{id}/payments` | List payments for invoice |
| GET | `/v1/payments/{id}` | Get payment attempt |
| POST | `/v1/payments/{id}/refunds` | Create refund |
| GET | `/v1/refunds/{id}` | Get refund |

---

### Auth
OAuth 2.0 Bearer tokens on every request. Scope examples: `billing:read`, `billing:write`.

---

### Idempotency
All `POST` requests require `Idempotency-Key: <client-uuid>` header. Server stores key → response for 24h. Replaying an identical key returns the cached response with `Idempotency-Replayed: true`. Mismatched body on replay returns `409 Conflict`.

---

### Request/Response Examples

**POST /v1/invoices/{id}/payments**
```json
// Request
{ "amount_cents": 4999, "currency": "USD", "payment_method_id": "pm_abc" }

// Response 201
{
  "id": "pay_xyz",
  "invoice_id": "inv_abc",
  "status": "succeeded",
  "amount_cents": 4999,
  "currency": "USD",
  "created_at": "2026-07-02T00:00:00Z"
}

Error schema (stable, machine-readable):

{ "error": { "code": "insufficient_funds", "message": "...", "param": null } }

code values are frozen per major version. Never remove or rename a code.


43.3.4 Pagination

All list endpoints use cursor-based pagination:

GET /v1/invoices?limit=20&starting_after=inv_abc

Response includes { "data": [...], "has_more": true, "next_cursor": "inv_xyz" }. Never use offset pagination — it breaks under concurrent inserts.


43.3.5 Versioning & Migration

  • URL-versioned (/v1/). Additive changes (new fields, new statuses) are non-breaking.
  • Breaking changes (removed fields, renamed codes) require /v2/.
  • Run both versions in parallel during transition; sunset v1 only after client cadence allows (12+ months minimum given slow-upgrade constraint).
  • New optional fields default to null on old records.

43.3.6 Race Conditions & Retry Hazards

Risk Mitigation
Double-charge on network retry Idempotency key + DB unique constraint on key
Payment succeeded but webhook lost Client polls GET /v1/payments/{id} before retrying
Refund on already-refunded payment Check refunded amount ≤ captured amount in DB transaction
Invoice voided mid-payment Pessimistic lock on invoice row during payment attempt

### claude-code-safe - weak r1

- Surface label: Claude Code CLI with tools disabled
- Run timestamp: `2026-07-02T08:30:55.866790+00:00`
- Prompt file: [examples/evaluations/runs/claude-code-safe/api-design/trap/r01-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/trap/r01-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/claude-code-safe/api-design/trap/r01-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/trap/r01-weak-output.md)
- Reviewability total: 5
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
## Billing API Design

### Core Principles

This design explicitly addresses the two most common production failure modes in billing APIs: **duplicate charges from retries** and **breaking client error handling across versions**.

---

### Idempotency

Every mutating operation requires a client-supplied `Idempotency-Key` header (UUID). The server stores the key and its response for 24 hours. Replaying a request with the same key returns the cached response with a `X-Idempotency-Replayed: true` header — no double-charge.

POST /v1/charges Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{ “customer_id”: “cus_123”, “amount_cents”: 4999, “currency”: “usd” }


Keys are scoped per API credential. A key collision from a *different* customer returns `409 Conflict`, not a silent merge.

---

### Stable Error Envelope

All error responses — now and in future versions — conform to this envelope:

```json
{
  "error": {
    "code": "card_declined",
    "message": "The card was declined.",
    "param": null,
    "request_id": "req_abc123"
  }
}

Stability contract: - error.code is a stable enum string; clients may switch on it. - error.message is human-readable and may change across versions — do not parse it. - New fields may be added; existing fields are never removed or renamed within a major version. - HTTP status codes map predictably: 402 for payment failure, 422 for validation, 409 for idempotency conflicts, 429 for rate limits, 5xx for server faults.


43.3.7 Versioning

URL-versioned (/v1/, /v2/). Old versions remain live for a minimum 12-month deprecation window. Version sunset is communicated via Deprecation and Sunset response headers on every request to a deprecated version.


43.3.8 Key Endpoints

Method Path Idempotency Required
POST /v1/charges Yes
POST /v1/refunds Yes
GET /v1/charges/{id} No
POST /v1/subscriptions Yes
DELETE /v1/subscriptions/{id} No

43.3.9 Retry Guidance

Clients should retry on 429 and 5xx with exponential backoff. Retries on 4xx (except 429) are safe only if the original request included an idempotency key — the server guarantees at-most-once execution for keyed requests.


What’s deliberately excluded: webhook delivery semantics and PCI scope are real concerns but require separate specs — listing them here without detail would be worse than deferring.


### claude-code-safe - repaired r1

- Surface label: Claude Code CLI with tools disabled
- Run timestamp: `2026-07-02T08:31:17.782396+00:00`
- Prompt file: [examples/evaluations/runs/claude-code-safe/api-design/trap/r01-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/trap/r01-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/claude-code-safe/api-design/trap/r01-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/claude-code-safe/api-design/trap/r01-repaired-output.md)
- Reviewability total: 8
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
## Billing API Design (v1)

### Assumptions (resource model was underspecified)

- Invoices are immutable once finalized; line items are set at creation.
- A PaymentAttempt is a child of Invoice; refunds are children of a PaymentAttempt.
- Idempotency keys are client-supplied, stored server-side for 24 hours minimum.
- OAuth scopes: `billing:read`, `billing:write`, `billing:admin`.

---

### Endpoint Table

| Method | Path | Scope | Description |
|--------|------|-------|-------------|
| GET | `/v1/invoices` | billing:read | List invoices (paginated) |
| POST | `/v1/invoices` | billing:write | Create invoice |
| GET | `/v1/invoices/{id}` | billing:read | Get invoice |
| POST | `/v1/invoices/{id}/payment-attempts` | billing:write | Attempt payment |
| GET | `/v1/invoices/{id}/payment-attempts` | billing:read | List attempts |
| POST | `/v1/payment-attempts/{id}/refunds` | billing:admin | Issue refund |

---

### Error Envelope (stable contract — never change field names)

```json
{
  "error": {
    "code": "PAYMENT_DECLINED",
    "message": "Card was declined by issuer.",
    "request_id": "req_abc123",
    "retryable": false
  }
}

retryable: true signals the client may safely replay. Always return this field — omission breaks retry logic in existing clients.


43.3.10 Idempotency

  • POST endpoints require Idempotency-Key: <uuid> header.
  • Server stores (key → response) in PostgreSQL for 72 hours.
  • Replayed requests return the exact original response (same body, same HTTP status).
  • If the same key arrives while the original is still processing: return 409 Conflict with code: IDEMPOTENCY_CONFLICT.
  • Keys are scoped to (oauth_client_id, key) — prevents cross-tenant collisions.

43.3.11 Pagination

{
  "data": [...],
  "pagination": {
    "cursor": "cur_xyz",
    "has_more": true
  }
}

Use cursor-based pagination only. Offset pagination is banned — it creates phantom/duplicate rows under concurrent writes.


43.3.12 Race Conditions & Retry Hazards

Risk Mitigation
Double-charge on network timeout Idempotency key deduplication at DB level (unique constraint on key)
Refund on already-refunded attempt Optimistic lock; check status = 'succeeded' before issuing
Concurrent payment attempts on same invoice DB-level advisory lock on invoice_id during attempt creation
Stale status read mid-payment Clients must poll; never trust local cache on terminal states

43.3.13 Versioning & Migration

  • Version in URL path (/v1/). Never in headers — harder to route and log.
  • Additive-only changes (new fields, new optional params) are non-breaking within v1.
  • Removing or renaming fields requires /v2/ with a minimum 12-month v1 sunset window — slow mobile upgrade cadence demands this.
  • v1 and v2 run simultaneously behind the same gateway; both write to the same PostgreSQL schema using views to isolate breaking changes.
  • Deprecation communicated via Deprecation and Sunset response headers on every v1 response once sunset is announced.

### codex-cli-default - weak r1

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T12:53:07.206640+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r01-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r01-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r01-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r01-weak-output.md)
- Reviewability total: 8
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
Missing details: billing model, pricing units, tax rules, currencies, payment provider, auth scheme, tenant model, invoice lifecycle, and compliance requirements. Below is a compatibility-safe billing API shape.

**Core Principles**

- All mutating requests support `Idempotency-Key`.
- Retries with the same key must return the original result, including success or failure.
- Error responses use one stable envelope across all endpoints.
- API versioning is explicit: `/v1/...`.
- New fields may be added, but existing fields and error codes are not removed or retyped.

**Endpoints**

```http
POST /v1/customers
POST /v1/subscriptions
GET  /v1/subscriptions/{subscription_id}
POST /v1/subscriptions/{subscription_id}/cancel
POST /v1/invoices
GET  /v1/invoices/{invoice_id}
POST /v1/invoices/{invoice_id}/pay
GET  /v1/payments/{payment_id}

Idempotency Contract

Required for all POST requests:

Idempotency-Key: 7f8c9b6e-...

Behavior:

  • Same key + same request body returns the same response.
  • Same key + different request body returns 409 idempotency_key_conflict.
  • Keys are scoped to account/tenant and endpoint.
  • Server stores the final response envelope for replay.
  • Clients may safely retry on network failures, 408, 409 idempotency conflict only after correction, 429, and 5xx.

Stable Error Envelope

{
  "error": {
    "code": "payment_declined",
    "message": "Payment was declined.",
    "request_id": "req_123",
    "retryable": false,
    "details": {
      "invoice_id": "inv_123"
    }
  }
}

Required fields are always present: error.code, error.message, error.request_id, error.retryable.

Error codes are stable API contract values, for example:

  • invalid_request
  • not_found
  • authentication_failed
  • authorization_failed
  • idempotency_key_conflict
  • payment_declined
  • invoice_already_paid
  • rate_limited
  • internal_error

Example: Pay Invoice

POST /v1/invoices/inv_123/pay
Idempotency-Key: pay-inv-123-attempt-1
{
  "payment_method_id": "pm_123"
}

Success:

{
  "payment": {
    "id": "pay_123",
    "invoice_id": "inv_123",
    "status": "succeeded"
  }
}

This design preserves retry safety and keeps clients insulated from breaking changes through explicit idempotency and stable error envelopes.


### codex-cli-default - repaired r1

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T12:53:30.570021+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r01-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r01-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r01-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r01-repaired-output.md)
- Reviewability total: 9
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
**Assumptions**

Billing resources are tenant-scoped. Invoices are created by backend/admin clients, payment attempts charge an invoice, and refunds target a successful payment attempt. Currency is ISO-4217 minor units.

**Endpoint Table**

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/v1/invoices` | List invoices, paginated |
| `POST` | `/v1/invoices` | Create invoice |
| `GET` | `/v1/invoices/{invoice_id}` | Fetch invoice |
| `POST` | `/v1/invoices/{invoice_id}/payment_attempts` | Attempt payment |
| `GET` | `/v1/payment_attempts/{attempt_id}` | Fetch payment attempt |
| `POST` | `/v1/payment_attempts/{attempt_id}/refunds` | Create refund |
| `GET` | `/v1/refunds/{refund_id}` | Fetch refund |

Pagination: `?limit=50&starting_after=inv_123`. Responses include:

```json
{
  "data": [],
  "has_more": false,
  "next_cursor": null
}

Create Invoice

POST /v1/invoices
Authorization: Bearer <token>
Idempotency-Key: 4f0b6b8a-...
Content-Type: application/json
{
  "customer_id": "cus_123",
  "currency": "usd",
  "amount_due": 2500,
  "metadata": { "order_id": "ord_456" }
}
{
  "id": "inv_123",
  "object": "invoice",
  "status": "open",
  "currency": "usd",
  "amount_due": 2500,
  "amount_paid": 0,
  "created_at": "2026-07-02T15:04:05Z"
}

Payment Attempt

{
  "payment_method_id": "pm_123",
  "amount": 2500
}

Response:

{
  "id": "pa_123",
  "object": "payment_attempt",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount": 2500
}

Stable Error Envelope

Never change this shape in /v1; only add optional fields.

{
  "error": {
    "type": "invalid_request",
    "code": "invoice_not_open",
    "message": "Invoice is not open.",
    "param": "invoice_id",
    "request_id": "req_123"
  }
}

Auth Notes

Use OAuth bearer tokens. Require scopes such as billing:read, billing:write, and tenant isolation from token claims. Return 401 for invalid tokens, 403 for insufficient scope.

Idempotency Notes

All unsafe writes require Idempotency-Key. Store key, tenant, method, path, request hash, status, and response body in PostgreSQL. Replaying the same key returns the original response, including 4xx validation errors. Same key with different body returns 409 idempotency_key_conflict. Keys expire after a documented window, e.g. 24 hours.

Migration / Versioning

Use path versions: /v1, /v2. In /v1, do not rename fields, remove fields, change enum meanings, or change error envelopes. Additive fields are allowed. New breaking behavior ships under /v2.

Risks

Race conditions: concurrent payment attempts can double-charge unless invoice payment is guarded by row locks or unique constraints on successful payment per invoice.

Retry hazards: clients may retry after timeout while the charge succeeded. Idempotency must return the original successful result.

Compatibility risks: mobile clients upgrade slowly, so error codes, enum values, pagination shape, timestamps, and id formats must remain stable in /v1.


### codex-cli-default - weak r2

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T12:53:54.228848+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r02-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r02-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r02-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r02-weak-output.md)
- Reviewability total: 6
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
Missing specifics: billing model, currencies, taxes, discounts, proration, payment processor, auth/tenant model, retry window, and webhook needs.

A compatibility-safe REST shape:

**Core Contract**
- Base path: `/v1`
- Auth: bearer token or service token, exact scheme TBD.
- All mutating requests require `Idempotency-Key`.
- Reusing the same key with the same payload returns the original status/body.
- Reusing the same key with a different payload returns a stable conflict error.
- Error response envelope is never changed incompatibly.

**Stable Error Envelope**
```json
{
  "error": {
    "code": "invoice_not_found",
    "type": "not_found",
    "message": "Invoice was not found.",
    "request_id": "req_123",
    "details": {}
  }
}

Resources

POST /v1/customers
GET /v1/customers/{customer_id}

POST /v1/invoices
GET /v1/invoices/{invoice_id}
POST /v1/invoices/{invoice_id}/finalize
POST /v1/invoices/{invoice_id}/void

POST /v1/payments
GET /v1/payments/{payment_id}
POST /v1/payments/{payment_id}/refunds

POST /v1/subscriptions
GET /v1/subscriptions/{subscription_id}
PATCH /v1/subscriptions/{subscription_id}
POST /v1/subscriptions/{subscription_id}/cancel

Example Mutating Request

POST /v1/payments
Idempotency-Key: pay_2026_07_02_order_123
Content-Type: application/json
{
  "customer_id": "cus_123",
  "invoice_id": "inv_123",
  "amount": 5000,
  "currency": "USD"
}

Compatibility Rules - Existing fields are never renamed, removed, or given new meanings. - New fields are additive and optional. - Enum expansion is versioned or explicitly documented because clients may switch on values. - Retryable failures use the same error envelope. - 5xx, 429, and network failures are safe to retry with the same Idempotency-Key. - Webhook/event delivery, if added, must also be idempotent and include stable event IDs.

This is intentionally conservative: billing clients often retry aggressively, so idempotency and stable errors are part of the API contract, not implementation details.


### codex-cli-default - repaired r2

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T12:54:25.519006+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r02-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r02-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r02-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r02-repaired-output.md)
- Reviewability total: 9
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
**Assumptions**

Invoices belong to an account/customer already modeled elsewhere. Payment attempts charge an invoice through a payment provider. Refunds apply only to succeeded payment attempts.

**Endpoint Table**

| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/v1/invoices?limit=50&cursor=...` | List invoices, newest first |
| `GET` | `/v1/invoices/{invoice_id}` | Fetch invoice |
| `GET` | `/v1/invoices/{invoice_id}/payment_attempts` | List attempts |
| `POST` | `/v1/invoices/{invoice_id}/payment_attempts` | Create payment attempt |
| `GET` | `/v1/payment_attempts/{attempt_id}` | Fetch attempt |
| `POST` | `/v1/payment_attempts/{attempt_id}/refunds` | Create refund |
| `GET` | `/v1/payment_attempts/{attempt_id}/refunds` | List refunds |
| `GET` | `/v1/refunds/{refund_id}` | Fetch refund |

**Pagination**

```json
{
  "data": [{ "id": "inv_123", "status": "open", "amount_due": 2500, "currency": "USD" }],
  "page": {
    "limit": 50,
    "next_cursor": "eyJpZCI6Imludl8xMjMifQ"
  }
}

Use opaque cursor pagination. Never expose offset as the primary contract.

Create Payment Attempt

Request:

POST /v1/invoices/inv_123/payment_attempts
Authorization: Bearer token
Idempotency-Key: 1db7b39e-6f0d-4b2d-bc85-9d1df5a7c111
Content-Type: application/json
{
  "payment_method_id": "pm_456",
  "amount": 2500,
  "currency": "USD"
}

Response:

{
  "id": "pa_789",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount": 2500,
  "currency": "USD",
  "created_at": "2026-07-02T15:04:05Z"
}

Create Refund

{
  "amount": 1000,
  "reason": "customer_request"
}
{
  "id": "rf_123",
  "payment_attempt_id": "pa_789",
  "status": "succeeded",
  "amount": 1000,
  "currency": "USD"
}

Stable Error Schema

All errors use the same envelope across versions:

{
  "error": {
    "code": "invoice_not_payable",
    "message": "Invoice cannot be paid in its current state.",
    "details": {
      "invoice_id": "inv_123",
      "status": "void"
    },
    "request_id": "req_abc",
    "doc_url": "https://api.example.com/docs/errors#invoice_not_payable"
  }
}

Do not rename error.code, message, details, or request_id.

Auth Notes

Use OAuth 2.0 bearer tokens. Required scopes: billing.invoices.read, billing.payments.write, billing.refunds.write. Authorize every resource by tenant/account ownership, not only by token validity.

Idempotency Notes

Require Idempotency-Key for all POST endpoints. Scope keys by tenant, method, path, and authenticated client. Store request hash and final response status/body in PostgreSQL. Retrying the same key with the same body returns the original response. Same key with different body returns 409 idempotency_key_reused. Concurrent duplicate requests return 409 idempotency_in_progress plus Retry-After.

Use unique constraints on idempotency records and provider transaction IDs.

Migration / Versioning Notes

Use /v1. Additive fields are allowed. Never remove or rename fields in v1. Avoid changing enum meanings. New enum values are compatibility risks for slow mobile clients, so prefer feature-gated rollout or documented unknown handling.

Breaking changes require /v2, parallel operation, migration docs, and long deprecation windows.

Verification Risks

Race conditions: double payment attempts, over-refunds, invoice state changes during payment. Use row locks or serializable transactions around invoice payment and refund totals.

Retry hazards: network timeout after provider charge but before client response. Persist idempotency state before external charge and reconcile by provider idempotency key.

Compatibility risks: changing error envelopes, pagination shape, status meanings, or required request fields will break old clients.


### codex-cli-default - weak r3

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T12:54:48.720193+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r03-weak-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r03-weak-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r03-weak-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r03-weak-output.md)
- Reviewability total: 7
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
Missing requirements: billing model, authentication, currencies, tax rules, payment provider, retry window, webhook needs, and compliance constraints.

A safe API shape should make compatibility explicit:

```http
POST /v1/billing/accounts
GET  /v1/billing/accounts/{account_id}

POST /v1/billing/invoices
GET  /v1/billing/invoices/{invoice_id}
POST /v1/billing/invoices/{invoice_id}/finalize
POST /v1/billing/invoices/{invoice_id}/void

POST /v1/billing/payments
GET  /v1/billing/payments/{payment_id}

POST /v1/billing/refunds
GET  /v1/billing/refunds/{refund_id}

All mutation endpoints must support idempotent retries:

Idempotency-Key: client-generated-unique-key

Semantics:

  • Same key + same authenticated client + same method + same path + same body returns the original response, including status and body.
  • Same key with a different body returns 409 idempotency_key_reused.
  • Idempotency records must live at least as long as the documented client retry window.
  • Clients can safely retry POST requests after timeouts or 5xx responses.
  • Server-generated side effects must happen at most once per accepted idempotency key.

Stable error envelope:

{
  "error": {
    "code": "invalid_request",
    "message": "Amount must be greater than zero.",
    "param": "amount",
    "request_id": "req_123",
    "type": "validation_error"
  }
}

Compatibility rules:

  • Never remove or rename response fields in /v1.
  • Additive fields are allowed.
  • Error code values are stable API contract, not free-form strings.
  • Clients should branch on error.code, not message.
  • Deprecated fields remain until a new major version.
  • Every response includes a request ID.
  • Money amounts use integer minor units plus explicit currency, for example { "amount": 1299, "currency": "USD" }.

Example payment request:

{
  "account_id": "acct_123",
  "invoice_id": "inv_123",
  "amount": 1299,
  "currency": "USD"
}

Example response:

{
  "id": "pay_123",
  "status": "succeeded",
  "amount": 1299,
  "currency": "USD",
  "invoice_id": "inv_123"
}

### codex-cli-default - repaired r3

- Surface label: Codex CLI default model
- Run timestamp: `2026-07-02T12:55:18.794149+00:00`
- Prompt file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r03-repaired-prompt.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r03-repaired-prompt.md)
- Transcript file: [examples/evaluations/runs/codex-cli-default/api-design/trap/r03-repaired-output.md](https://github.com/corbensorenson/software-grimoire/blob/main/examples/evaluations/runs/codex-cli-default/api-design/trap/r03-repaired-output.md)
- Reviewability total: 11
- Outcome total: 3
- Evaluator notes: Auto-scored with outcome checks and a secondary reviewability rubric; transcript remains the primary evidence.

```text
**Assumptions**

Invoices belong to an authenticated tenant/account. Money is stored in minor units with ISO currency. Payment attempts charge an invoice. Refunds apply to successful payment attempts.

**Endpoint Table**

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/v1/invoices` | List invoices with cursor pagination |
| `POST` | `/v1/invoices` | Create draft invoice |
| `GET` | `/v1/invoices/{invoice_id}` | Fetch invoice |
| `POST` | `/v1/invoices/{invoice_id}/finalize` | Finalize invoice |
| `GET` | `/v1/payment_attempts` | List payment attempts |
| `POST` | `/v1/payment_attempts` | Attempt payment for invoice |
| `GET` | `/v1/payment_attempts/{attempt_id}` | Fetch attempt |
| `GET` | `/v1/refunds` | List refunds |
| `POST` | `/v1/refunds` | Create refund |
| `GET` | `/v1/refunds/{refund_id}` | Fetch refund |

**Examples**

Create payment attempt:

```http
POST /v1/payment_attempts
Authorization: Bearer <token>
Idempotency-Key: 9b7c1f1a-7e9a-4c0d-bdf0-7e3c8b5b8c22
Content-Type: application/json
{
  "invoice_id": "inv_123",
  "amount": 2500,
  "currency": "USD",
  "payment_method_id": "pm_456"
}
{
  "id": "pa_789",
  "invoice_id": "inv_123",
  "status": "succeeded",
  "amount": 2500,
  "currency": "USD",
  "created_at": "2026-07-02T18:30:00Z"
}

List response:

{
  "data": [
    {
      "id": "inv_123",
      "status": "open",
      "amount_due": 2500,
      "currency": "USD"
    }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6Imludl8xMjMifQ",
    "has_more": true
  }
}

Stable error envelope:

{
  "error": {
    "code": "idempotency_key_conflict",
    "message": "Idempotency key was reused with a different request body.",
    "request_id": "req_abc",
    "field": null,
    "retryable": false
  }
}

Auth Notes

Use OAuth bearer tokens. Required scopes: billing:read, billing:write, payments:write, refunds:write. Enforce tenant isolation on every query using tenant/account ID from the token, never from client input.

Idempotency Notes

All mutating POST endpoints require Idempotency-Key. Store (tenant_id, method, path, key, request_hash, response_status, response_body) in PostgreSQL. Same key plus same request returns the original response, including errors. Same key with different body returns 409 idempotency_key_conflict.

For in-flight duplicates, either block until the first request completes or return 409 idempotency_in_progress with retryable: true. Never create a second charge/refund after a client timeout.

Migration / Versioning Notes

Use /v1. Backward-compatible changes are additive only: new nullable fields, new endpoints, new enum values only if clients are documented to ignore unknowns. Do not change existing field meanings, error shape, pagination shape, or idempotency behavior. Breaking changes require /v2 and long deprecation windows because clients upgrade slowly.

Verification Risks

Race conditions: double payment attempts for one invoice, refund total exceeding captured amount, and concurrent finalize/pay flows. Use transactions, row locks, unique constraints, and refund-sum checks.

Retry hazards: network timeout after successful charge, provider webhook arriving before API response, duplicate mobile retries.

Compatibility risks: changing error codes, removing fields, changing cursor format, shortening idempotency retention, or returning a different response for repeated idempotent requests. ```