API_CONTRACTS — payment-gateway-service
Sibling: APPLICATION_LOGIC · DOMAIN_MODEL · SECURITY_MODEL · EVENT_SCHEMAS
Strategic anchors: 05 API Design · 10 Payments Architecture · standards/ERROR_CODES
All endpoints are versioned at /api/v1/payments. Webhook receivers are at /webhooks/v1/<vendor> on a separate subdomain (webhooks.payments.melmastoon.ghasi.io) behind Cloud Armor. Request/response bodies are JSON; errors are RFC 7807 Problem+JSON. Money is serialized as { "amountMicro": "12500000", "currency": "USD" } (string for bigint over JSON). Timestamps are ISO-8601 UTC. All mutating endpoints require Idempotency-Key and propagate traceparent.
This service is fronted by bff-tenant-booking-service (guest funnel — tokenization, intent), bff-backoffice-service (front-desk — cash, refunds, chargebacks), and called server-to-server by reservation-service (saga-driven authorize / capture / refund / void). Direct guest traffic to this service is denied at the gateway.
1. Common request headers
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <jwt> | yes | Issued by iam-service; service tokens for s2s callers |
X-Tenant-Id: tnt_… | yes | Cross-checked against JWT; mismatch → 403 MELMASTOON.TENANT.NOT_A_MEMBER |
X-Property-Id: ppt_… | for staff endpoints | Validated against staff property access |
X-Request-Id: <ULID> | recommended | Returned in requestId of error envelope |
Idempotency-Key: <ULID> | yes for POST / DELETE | 24 h dedupe window; same key + different body → 409 MELMASTOON.SYNC.IDEMPOTENCY_KEY_REUSED |
Accept-Language: <bcp47> | recommended | Drives Problem+JSON title localization |
traceparent: 00-…-…-01 | yes (gateway-set) | W3C trace propagation |
2. Common error envelope
Per ERROR_CODES. Example:
{
"error": {
"type": "https://errors.melmastoon.ghasi.io/payment/declined",
"code": "MELMASTOON.PAYMENT.DECLINED",
"title": "The payment was declined.",
"status": 402,
"detail": "Issuer declined the payment instrument. No further detail safe to disclose.",
"instance": "/api/v1/payments/intents",
"requestId": "01HZX8QF…",
"traceId": "00-1a2b3c…",
"tenantId": "tnt_01H…",
"retriable": false,
"userMessageKey": "errors.payment.declined",
"docUrl": "https://docs.melmastoon.ghasi.io/errors/payment/declined",
"runbook": "https://runbooks.melmastoon.ghasi.io/payment/declined"
}
}
| Status | Used for |
|---|---|
400 | malformed request |
401 | missing/invalid JWT or webhook signature |
402 | declined / insufficient funds |
403 | tenant mismatch, RBAC denial |
404 | not found (or RLS-hidden) |
409 | OCC, illegal state transition, idempotency-key reused with different body |
410 | tokenization session expired, authorization expired |
422 | domain invariant violation |
429 | rate-limited |
500 | unhandled (paged) |
502 | adapter unavailable / circuit open |
504 | gateway timeout (vendor SDK) |
3. Payment intents (authorize / capture / refund / void)
3.1 POST /api/v1/payments/intents
Create a payment intent — runs AuthorizePayment (APPLICATION_LOGIC §2.1).
Caller: reservation-service (saga), bff-tenant-booking-service (guest funnel direct path for staff-assisted bookings).
Request
{
"reservationId": "rsv_01H3Z…",
"propertyId": "ppt_01H3Z…",
"guestId": "gst_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"method": {
"kind": "card",
"paymentMethodId": "pm_01HZX…",
"metadata": { "device_fp": "abc123" }
},
"fxContext": {
"base": "USD",
"quote": "AFN",
"rate": 71.50,
"source": "ecb",
"provider": "ecb",
"quotedAt": "2026-04-22T18:31:00Z",
"snapshotId": "fxs_01HZX…"
},
"capture": "manual",
"description": "Reservation GM-9F4K2C — 3 nights at Property Kabul Riverside",
"initiatedBy": { "type": "guest", "id": "gst_01H3Z…" }
}
Response 201
{
"paymentId": "pay_01HZX…",
"authorizationId": "auth_01HZX…",
"status": "authorized",
"processor": "stripe",
"expiresAt": "2026-04-29T18:31:00Z",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"fxContext": { "...": "..." },
"createdAt": "2026-04-22T18:31:00.123Z"
}
Response 200 (requires action — 3DS)
{
"paymentId": "pay_01HZX…",
"authorizationId": "auth_01HZX…",
"status": "requires_action",
"processor": "stripe",
"requiresAction": {
"type": "3ds_redirect",
"url": "https://hooks.stripe.com/3d_secure/…",
"ref": "src_1NXz…",
"expiresAt": "2026-04-22T18:46:00Z"
}
}
Errors: 402 MELMASTOON.PAYMENT.DECLINED · 402 MELMASTOON.PAYMENT.INSUFFICIENT_FUNDS · 422 MELMASTOON.PRICING.CURRENCY_MISMATCH · 502 MELMASTOON.PAYMENT.GATEWAY_TIMEOUT (retriable) · 409 MELMASTOON.SYNC.IDEMPOTENCY_KEY_REUSED.
3.2 POST /api/v1/payments/intents/:paymentId/capture
Capture (full or partial). Caller typically reservation-service on confirmed.v1 or at check-in for deposit.
Request
{
"amount": { "amountMicro": "560000000", "currency": "USD" }, // omit for full capture
"reason": "deposit_at_checkin"
}
Response 200
{
"paymentId": "pay_01HZX…",
"captureId": "cap_01HZX…",
"status": "captured",
"capturedAt": "2026-04-22T18:31:42.815Z",
"amount": { "amountMicro": "560000000", "currency": "USD" }
}
Errors: 404 MELMASTOON.PAYMENT.INTENT_NOT_FOUND · 409 MELMASTOON.PAYMENT.INVALID_STATE_TRANSITION (e.g., already captured) · 410 MELMASTOON.PAYMENT.AUTHORIZATION_EXPIRED · 502 MELMASTOON.PAYMENT.GATEWAY_TIMEOUT.
3.3 POST /api/v1/payments/intents/:paymentId/void
Void within the void window. Releases the authorization.
Request
{ "reason": "saga_compensation" }
Response 204 (no body).
Errors: 404 MELMASTOON.PAYMENT.INTENT_NOT_FOUND · 409 MELMASTOON.PAYMENT.INVALID_STATE_TRANSITION · 422 MELMASTOON.PAYMENT.VOID_WINDOW_ELAPSED.
3.4 POST /api/v1/payments/intents/:paymentId/refunds
Create a refund (full or partial). Caller decides amount + reason — typically reservation-service after applying its refund-policy DSL.
Request
{
"amount": { "amountMicro": "200000000", "currency": "USD" },
"reason": "cancellation_within_policy",
"note": "Cancellation 5 days before arrival, flexible_72h policy"
}
Response 200
{
"refundId": "rfd_01HZX…",
"paymentId": "pay_01HZX…",
"status": "refunded",
"amount": { "amountMicro": "200000000", "currency": "USD" },
"reason": "cancellation_within_policy",
"refundedAt": "2026-04-22T18:32:10.001Z"
}
Errors: 422 MELMASTOON.BILLING.REFUND_EXCEEDS_BALANCE · 502 MELMASTOON.PAYMENT.GATEWAY_TIMEOUT · 404 MELMASTOON.PAYMENT.INTENT_NOT_FOUND.
A refund whose adapter returned pending returns status: "pending" with 202 Accepted; the final state is delivered via payment.transaction.refunded.v1 after the matching webhook.
3.5 GET /api/v1/payments/intents/:paymentId
Fetch a transaction with full event timeline.
Response 200
{
"paymentId": "pay_01HZX…",
"tenantId": "tnt_01H…",
"reservationId": "rsv_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "USD" },
"status": "captured",
"method": "card",
"processor": "stripe",
"fxContext": { "...": "..." },
"authorization": { "id": "auth_01HZX…", "expiresAt": "2026-04-29T18:31:00Z" },
"captures": [{ "id": "cap_01HZX…", "amount": { "amountMicro": "560000000", "currency": "USD" }, "capturedAt": "2026-04-22T18:31:42Z" }],
"refunds": [],
"events": [
{ "at": "2026-04-22T18:31:00Z", "type": "created" },
{ "at": "2026-04-22T18:31:01Z", "type": "authorized", "processorRef": "pi_3NXzABCdef" },
{ "at": "2026-04-22T18:31:42Z", "type": "captured", "processorRef": "ch_3NXzABCdef" }
],
"createdAt": "2026-04-22T18:31:00Z",
"updatedAt": "2026-04-22T18:31:42Z",
"version": 3
}
GET /api/v1/payments/transactions/:id is an alias used by billing-service callers.
4. Payment methods (tokenization)
4.1 POST /api/v1/payments/payment-methods/sessions
Create a hosted-fields tokenization session. The client uses clientSecret with the processor's hosted-fields SDK (Stripe Elements confirmSetup, PayPal hosted fields).
Request
{
"guestId": "gst_01H3Z…",
"method": "card",
"returnUrl": "https://book.kabulriverside.com/payment/return"
}
Response 201
{
"sessionId": "psess_01HZX…",
"processor": "stripe",
"clientSecret": "seti_3NXz…_secret_abc123",
"expiresAt": "2026-04-22T18:46:00Z"
}
The clientSecret is consumed client-side by the processor SDK. We never see PAN.
4.2 POST /api/v1/payments/payment-methods
Server-confirm attachment after the hosted-fields client confirms. Triggered also by webhook (setup_intent.succeeded); this endpoint is the synchronous variant for guest funnels that prefer a deterministic UX over webhook-wait.
Request
{
"guestId": "gst_01H3Z…",
"sessionId": "psess_01HZX…",
"processorToken": "pm_1NXzABCdef"
}
Response 201
{
"id": "pm_01HZX…",
"kind": "card",
"processor": "stripe",
"display": { "brand": "visa", "last4": "4242", "expMonth": 12, "expYear": 2028 },
"status": "active",
"createdAt": "2026-04-22T18:32:00Z"
}
4.3 DELETE /api/v1/payments/payment-methods/:id
Detach (server-side reference deleted; processor token deletion enqueued). 204 on success.
4.4 GET /api/v1/payments/payment-methods?guestId=…
List active payment methods for a guest. Display details only; processor tokens never returned.
5. Cash endpoints
5.1 POST /api/v1/payments/cash/receipts
Record a cash receipt at the front desk. Available offline via desktop sync push.
Request
{
"reservationId": "rsv_01H3Z…",
"guestId": "gst_01H3Z…",
"amount": { "amountMicro": "560000000", "currency": "AFN" },
"operatorId": "usr_01H…",
"cashDrawerShiftId": "shf_01H…",
"fxContext": null,
"receiptPhotoMediaId": "med_01H…",
"occurredAt": "2026-04-22T18:35:00Z",
"deviceContext": { "deviceId": "dev_01H…", "offlineAt": null }
}
Response 201
{
"paymentId": "pay_01HZX…",
"captureId": "cap_01HZX…",
"status": "captured",
"method": "cash_on_arrival",
"processor": "cash",
"createdAt": "2026-04-22T18:35:00Z",
"syncStatus": "synced"
}
Errors: 422 MELMASTOON.RESERVATION.NOT_FOUND · 422 MELMASTOON.PAYMENT.CASH_DRAWER_NOT_OPEN.
5.2 POST /api/v1/payments/cash/refunds
Record a cash refund. Requires dual sign-off if amount exceeds tenant threshold.
Request
{
"paymentId": "pay_01HZX…",
"amount": { "amountMicro": "200000000", "currency": "AFN" },
"reason": "cancellation_within_policy",
"operatorId": "usr_01H…",
"secondOperatorId": "usr_01H…OTHER",
"cashDrawerShiftId": "shf_01H…"
}
Response 200
{
"refundId": "rfd_01HZX…",
"paymentId": "pay_01HZX…",
"status": "refunded",
"amount": { "amountMicro": "200000000", "currency": "AFN" },
"refundedAt": "2026-04-22T18:36:00Z"
}
Errors: 422 MELMASTOON.PAYMENT.DUAL_SIGNOFF_REQUIRED · 422 MELMASTOON.BILLING.REFUND_EXCEEDS_BALANCE.
6. Reconciliation
6.1 POST /api/v1/payments/reconciliations/run
Ad-hoc reconciliation (ops only — RBAC payments.reconciliation.run).
Request
{ "processor": "stripe", "date": "2026-04-21" }
Response 202
{ "reconciliationId": "rec_01HZX…", "status": "in_progress" }
6.2 GET /api/v1/payments/reconciliations/:id
Fetch a reconciliation report.
Response 200
{
"id": "rec_01HZX…",
"tenantId": "tnt_01H…",
"processor": "stripe",
"date": "2026-04-21",
"status": "completed",
"matched": { "count": 412, "total": { "amountMicro": "412000000000", "currency": "AFN" }},
"unmatched": { "count": 1, "total": { "amountMicro": "560000000", "currency": "AFN" },
"entries": [
{ "side": "platform_only", "paymentId": "pay_01HZX…", "amount": { "amountMicro": "560000000", "currency": "AFN" }, "reason": "webhook_drop_suspected" }
]},
"fees": { "amountMicro": "12450000000", "currency": "AFN" },
"net": { "amountMicro": "399550000000", "currency": "AFN" },
"source": { "reportId": "su_2NXzABC", "ingestedAt": "2026-04-22T02:03:11Z", "reportUri": "gs://gm-payments-reports/tnt_01H…/stripe/2026-04-21.csv" },
"completedAt": "2026-04-22T02:05:43Z"
}
7. Chargebacks
7.1 GET /api/v1/payments/chargebacks?status=open
List chargebacks filtered by status. Returns paginated.
7.2 POST /api/v1/payments/chargebacks/:id/evidence
Submit evidence pack.
Request (multipart/form-data)
bundle— ZIP of evidence files (booking, lock access logs, folio, guest comms).narrative— required text.aiAssist— boolean; if true,aiProvenanceIdmust be a HITL-approved decision id.
Response 200
{
"chargebackId": "cbk_01HZX…",
"status": "evidence_submitted",
"submittedAt": "2026-04-22T18:40:00Z",
"bundleRef": "gs://gm-disputes/tnt_01H…/cbk_01HZX…/bundle.zip",
"aiProvenanceId": "dec_01HZX…"
}
8. Adapter health
8.1 GET /api/v1/payments/adapters/health
Per-tenant adapter health snapshot.
Response 200
{
"tenantId": "tnt_01H…",
"adapters": [
{ "processor": "stripe", "circuitState": "closed", "p99LatencyMs": 812, "errorRate1m": 0.002, "lastSuccessAt": "2026-04-22T18:30:55Z" },
{ "processor": "paypal", "circuitState": "closed", "p99LatencyMs": 1124, "errorRate1m": 0.011, "lastSuccessAt": "2026-04-22T18:29:11Z" },
{ "processor": "hesabpay", "circuitState": "half_open", "p99LatencyMs": 4310, "errorRate1m": 0.184, "lastErrorAt": "2026-04-22T18:30:01Z" },
{ "processor": "cash", "circuitState": "closed", "p99LatencyMs": 3, "errorRate1m": 0.000 }
]
}
9. Webhook receivers
Each receiver lives at /webhooks/v1/<vendor> on the dedicated webhooks.payments.melmastoon.ghasi.io subdomain behind Cloud Armor + signature verification. Receivers always return 202 on signature-valid envelopes; processing is async (APPLICATION_LOGIC §2.6).
9.1 POST /webhooks/v1/stripe
Headers: Stripe-Signature: t=…,v1=…. Body: raw Stripe event JSON. Receiver:
- Reads
Stripe-Signature, validates against Secret Manager-resolved signing secret per (tenant, env). - Persists envelope to
webhook_inboxkeyed by(processor='stripe', externalEventId=event.id). - Returns
202 Acceptedwith{ "received": true }. - Dispatcher worker drains and applies state transitions.
Errors: 401 MELMASTOON.PAYMENT.WEBHOOK_SIGNATURE_INVALID.
9.2 POST /webhooks/v1/paypal
Headers: Paypal-Transmission-Sig, Paypal-Cert-Url, Paypal-Auth-Algo. Body: raw PayPal event. Receiver verifies via PayPal's certificate-chain algorithm.
9.3 POST /webhooks/v1/hesabpay
Headers: X-HesabPay-Signature: hmac_sha256=<hex>. Body: raw HesabPay event. Receiver verifies via HMAC against per-tenant Secret Manager secret.
9.4 Future: /webhooks/v1/adyen, /webhooks/v1/razorpay, /webhooks/v1/mpesa
Same shape; per-vendor signature scheme.
9.5 Webhook envelope (canonical persisted shape)
{
"id": "whk_01HZX…",
"processor": "stripe",
"externalEventId": "evt_3NXzABCdef",
"tenantId": "tnt_01H…", // resolved from event metadata
"signatureValid": true,
"receivedAt": "2026-04-22T18:31:50.001Z",
"rawPayloadRef": "gs://gm-webhooks/stripe/2026-04-22/evt_3NXzABCdef.json.enc",
"headers": { "stripe-signature": "t=…,v1=…", "user-agent": "Stripe/1.0" },
"status": "received",
"attempts": 0
}
10. Pagination, filtering, sorting
All list endpoints follow 05 API Design §pagination: cursor-based via ?cursor=…&limit=… (default 50, max 200). Filters use query params (?status=open&processor=stripe). Default sort is createdAt desc.
11. OpenAPI
The OpenAPI 3.1 document is generated from controllers via nestjs-zod-openapi and committed at services/payment-gateway-service/openapi.json (in the application monorepo). The OpenAPI diff gate fails any PR introducing a breaking change without bumping /api/v1 → /api/v2.