RESTful API Design for Payments
Versioning: Use URL path versioning (/v1/payments) for clarity. Header versioning (Accept: application/vnd.gnosispay.v1+json) is more "pure" but harder to debug. Stripe, the gold standard for payment APIs, uses path versioning.
Pagination: Cursor-based for real-time data (payment lists change as new payments arrive). Offset-based breaks when items are inserted/deleted between pages.
GET /v1/payments?cursor=pay_abc123&limit=25
Response: { data: [...], has_more: true, next_cursor: "pay_xyz789" }
Idempotent POST Requests: Critical for payment APIs. Client provides Idempotency-Key header. Server stores (key → response) in cache/DB. Same key within 24h returns cached response.
POST /v1/payments/authorize
Idempotency-Key: unique-client-generated-uuid
If network fails mid-response, client safely retries. Without this, you risk double-charging.
Error Contracts: Consistent error format across all endpoints.
{
"error": {
"type": "invalid_request",
"code": "insufficient_balance",
"message": "Available balance (45.00 EUR) is less than requested amount (100.00 EUR)",
"param": "amount",
"request_id": "req_abc123"
}
}
Use HTTP status codes correctly: 400 (bad input), 401 (no auth), 403 (no permission), 404 (not found), 409 (conflict/duplicate), 422 (validation), 429 (rate limit), 500 (server error).
Webhook Design: Thin payloads — send event type + resource ID, let the partner fetch details. This avoids stale data in webhook payloads and reduces payload size.
{
"event": "payment.settled",
"resource_id": "pay_abc123",
"resource_url": "/v1/payments/pay_abc123"
}Key Points
- ▸Path versioning (/v1/) for payment APIs — industry standard
- ▸Cursor-based pagination for real-time data consistency
- ▸Idempotency-Key header prevents double-charging on retry
- ▸Consistent error contract with type, code, message, request_id
- ▸Thin webhook payloads: event + resource ID, not full object