Skip to main content

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

HeaderRequiredNotes
Authorization: Bearer <jwt>yesIssued by iam-service; service tokens for s2s callers
X-Tenant-Id: tnt_…yesCross-checked against JWT; mismatch → 403 MELMASTOON.TENANT.NOT_A_MEMBER
X-Property-Id: ppt_…for staff endpointsValidated against staff property access
X-Request-Id: <ULID>recommendedReturned in requestId of error envelope
Idempotency-Key: <ULID>yes for POST / DELETE24 h dedupe window; same key + different body → 409 MELMASTOON.SYNC.IDEMPOTENCY_KEY_REUSED
Accept-Language: <bcp47>recommendedDrives Problem+JSON title localization
traceparent: 00-…-…-01yes (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"
}
}
StatusUsed for
400malformed request
401missing/invalid JWT or webhook signature
402declined / insufficient funds
403tenant mismatch, RBAC denial
404not found (or RLS-hidden)
409OCC, illegal state transition, idempotency-key reused with different body
410tokenization session expired, authorization expired
422domain invariant violation
429rate-limited
500unhandled (paged)
502adapter unavailable / circuit open
504gateway 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, aiProvenanceId must 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:

  1. Reads Stripe-Signature, validates against Secret Manager-resolved signing secret per (tenant, env).
  2. Persists envelope to webhook_inbox keyed by (processor='stripe', externalEventId=event.id).
  3. Returns 202 Accepted with { "received": true }.
  4. 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.