API & State Machine

The Setula backend exposes a REST API over raw node:http. Every mutation endpoint requires an Idempotency-Key header. The payout callback additionally requires an X-Payout-Callback-Secret header.

API routes

MethodRoutePurpose
GET/healthHealth check
POST/api/beneficiariesCreate the India beneficiary
POST/api/invoicesCreate the INR invoice
POST/api/invoices/:invoiceId/quotesGenerate the sandbox AED/INR quote
POST/api/paymentsCreate the payment intent in QUOTED
POST/api/payments/:paymentId/funding-confirmationsConfirm simulated AED funding
POST/api/payments/:paymentId/settlementsExecute the Arc USDC transfer
POST/api/payout-callbacksSimulate INR payout delivery or rejection
POST/api/payments/:paymentId/demo-payoutsDemo bridge to the simulated payout callback
GET/api/payments/:paymentIdRead payment and linked objects with timeline
GET/api/receipts/:receiptIdRead the invoice-linked receipt
i

Amounts ending in Minor are integer minor units: paise for INR and fils for AED. The sandbox rate is fixed at 22.75 INR/AED. The demo invoice of INR 91,000 maps to a real 0.01 USDC Arc Testnet settlement.

Data model

The backend manages seven entity types:

EntityKey fields
Beneficiaryid, name, email, bankAccountLast4
Invoiceid, beneficiaryId, reference, amountInrMinor
Quoteid, invoiceId, amountAedMinor, usdcAmount, rateInrPerAed
Paymentid, status, timeline, settlementId, payoutId, receiptId
Settlementid, circleTransactionId, transactionHash, arcScanUrl
Payoutid, reference, status (DELIVERED / REJECTED)
Receiptid, paymentId, invoiceId, transactionHash, arcScanUrl

Payment state machine

Every payment follows strict state-transition rules. Invalid transitions produce a 409 Conflict response.

text
DRAFT
  └──→ QUOTED
         └──→ FUNDED
                └──→ SETTLEMENT_PENDING
                       ├──→ SETTLED             (Circle status COMPLETE)
                       │     └──→ PAYOUT_PENDING
                       │            ├──→ DELIVERED        (payout callback)
                       │            └──→ PAYOUT_REJECTED
                       └──→ SETTLEMENT_FAILED   (insufficient balance)

Transition rules

FromToCondition
SETTLEMENT_PENDINGSETTLEDCircle reports COMPLETE with transaction hash
SETTLEMENT_PENDINGSETTLEMENT_FAILEDInsufficient sender balance or Circle transfer error
PAYOUT_PENDINGDELIVEREDAuthenticated payout callback with DELIVERED status
PAYOUT_PENDINGPAYOUT_REJECTEDAuthenticated payout callback with REJECTED status
SETTLEMENT_FAILEDREFUND_PENDINGOperator-initiated refund flow (future)
PAYOUT_REJECTEDREFUND_PENDINGOperator-initiated refund flow (future)
✕

Failed settlements produce no ArcScan link. The SETTLEMENT_FAILED state has no transactionHash or arcScanUrl. The payout callback is rejected with 409 Conflict. The recipient balance is never changed.

Idempotency

Every POST endpoint requires a unique Idempotency-Key header. Repeating the same request with the same key returns the original result without side effects.

Key guarantees:

  • Settlement: Duplicate settlement submissions reuse the existing Circle transaction. The transaction hash is preserved and the recipient balance does not change (delta 0).
  • Funding: Duplicate funding confirmations return the already-confirmed payment.
  • Payout: Duplicate payout callbacks return the already-recorded result.
✓

Validated: Duplicate-prevention was tested in the deployed environment. The recipient balance increased by exactly 0.01 USDC on the first settlement and 0 on all subsequent attempts.