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
| Method | Route | Purpose |
|---|---|---|
GET | /health | Health check |
POST | /api/beneficiaries | Create the India beneficiary |
POST | /api/invoices | Create the INR invoice |
POST | /api/invoices/:invoiceId/quotes | Generate the sandbox AED/INR quote |
POST | /api/payments | Create the payment intent in QUOTED |
POST | /api/payments/:paymentId/funding-confirmations | Confirm simulated AED funding |
POST | /api/payments/:paymentId/settlements | Execute the Arc USDC transfer |
POST | /api/payout-callbacks | Simulate INR payout delivery or rejection |
POST | /api/payments/:paymentId/demo-payouts | Demo bridge to the simulated payout callback |
GET | /api/payments/:paymentId | Read payment and linked objects with timeline |
GET | /api/receipts/:receiptId | Read the invoice-linked receipt |
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:
| Entity | Key fields |
|---|---|
| Beneficiary | id, name, email, bankAccountLast4 |
| Invoice | id, beneficiaryId, reference, amountInrMinor |
| Quote | id, invoiceId, amountAedMinor, usdcAmount, rateInrPerAed |
| Payment | id, status, timeline, settlementId, payoutId, receiptId |
| Settlement | id, circleTransactionId, transactionHash, arcScanUrl |
| Payout | id, reference, status (DELIVERED / REJECTED) |
| Receipt | id, paymentId, invoiceId, transactionHash, arcScanUrl |
Payment state machine
Every payment follows strict state-transition rules. Invalid transitions produce a 409 Conflict response.
DRAFT
└──→ QUOTED
└──→ FUNDED
└──→ SETTLEMENT_PENDING
├──→ SETTLED (Circle status COMPLETE)
│ └──→ PAYOUT_PENDING
│ ├──→ DELIVERED (payout callback)
│ └──→ PAYOUT_REJECTED
└──→ SETTLEMENT_FAILED (insufficient balance)Transition rules
| From | To | Condition |
|---|---|---|
SETTLEMENT_PENDING | SETTLED | Circle reports COMPLETE with transaction hash |
SETTLEMENT_PENDING | SETTLEMENT_FAILED | Insufficient sender balance or Circle transfer error |
PAYOUT_PENDING | DELIVERED | Authenticated payout callback with DELIVERED status |
PAYOUT_PENDING | PAYOUT_REJECTED | Authenticated payout callback with REJECTED status |
SETTLEMENT_FAILED | REFUND_PENDING | Operator-initiated refund flow (future) |
PAYOUT_REJECTED | REFUND_PENDING | Operator-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.