Download OpenAPI specification:Download
DRAFT
SFEC payout orchestration. Payload matches the OCBC payout format. sfec verifies proxyValue against merchant UEN and amount against the Collections transaction before proceeding.
Steps: transaction lookup → Workday budget check → journal → earmark cancel → wallet-core PayNow (OCBC) → reverse journal → adhoc bank transaction.
Option B flow: The E1 earmark is cancelled before PayNow fires, releasing the held balance so that makePaynowCreate can deduct from the free balance. PayNow uses saga.ID as idempotency key to prevent duplicate payments on retry.
Auth: Cognito client-credentials JWT. Scopes: wallet-core/sfec:payout:create (POST /sfec/payout/create) and wallet-core/sfec:payout:enquire (GET /sfec/payout/{govwalletTransactionId}). sfec uses its own service-to-service token for Collections reads — callers do not need wallet-core/transaction:read.
JWE Envelope: POST request bodies, and all responses except 401/403, use JWE (JSON Web Encryption) with RSA-OAEP-512 + AES-256-GCM.
application/x-www-form-urlencoded with a single field payload containing the JWE compact serialization of the JSON request body.payload parameter. SFEC ignores it.Content-Type: application/jose. Decrypt with the private key to obtain the JSON response body.Idempotency: Idempotency-Key header required. Same key resumes from last persisted step.
Account status: Every POST /sfec/payout/create request (new or retry with the same Idempotency-Key, whatever the payout's status, including pending_settlement) first checks the customer's wallet account. It returns 400 CUSTOMER_ACCOUNT_NOT_FOUND if the account does not exist, 422 CUSTOMER_ACCOUNT_DEACTIVATED if it is DEACTIVATED, or 500 CUSTOMER_ACCOUNT_LOOKUP_FAILED if the status could not be verified. In each case the existing payout is left unchanged.
Settlement: OCBC PayNow settles in up to 2170s. Returns 202 pending_settlement with pollAfterSeconds: 60; retry POST /sfec/payout/create with the same Idempotency-Key after 2170s. If still pending_settlement after 2170s, stop retrying and escalate manually.
Failed PayNow payment (rsfec only): If OCBC rejects or cancels the PayNow payment, the API returns 422 PAYNOW_FAILED_FUNDS_HELD, not 422 STEP_FAILED. The payout amount is held, not returned to the customer's balance. Wait at least 24 hours, then retry POST /sfec/payout/create with the same Idempotency-Key and the same request body. GovWallet makes one new PayNow payment with the held amount. A retry within 24 hours returns the same 422 and makes no new payment.
EARMARK_EXPIRED (422, in data.error.code): the earmark's cartExpiry has passed. TRANSACTION_INVALID_STATE is now also returned when the earmark is cancelled or pending cancel. Both are checked at transaction lookup, before any Workday step, and are final: a retry with the same Idempotency-Key returns the same failurePAYNOW_FAILED_FUNDS_HELD (422, rsfec only): OCBC rejected or cancelled the PayNow payment and the payout amount is held. Retry with the same Idempotency-Key after at least 24 hours to make one new payment. A retry within 24 hours returns the same 422OCBC_PAYNOW_FAILURE from the error code enum and the 502 examples. SFEC does not return itpollAfterSeconds in the settlement description from 2170 to 60POST request bodies use JWE. GET /sfec/payout/{govwalletTransactionId} has no request body, and SFEC ignores any payload parameter on it. No API behaviour changePOST /sfec/payout/create checks the proxyValue format for its proxyType before any saga write. A value in the wrong format returns 400 VALIDATION_ERROR with details.field = proxyValue. The response does not include the valueproxyType accepts only U, M or N. The long forms UEN, MOBILE and NRIC are no longer accepted and return 422 INVALID_PROXY_TYPEPOST /sfec/payout/create request — 400 CUSTOMER_ACCOUNT_NOT_FOUND when the account does not exist; 422 CUSTOMER_ACCOUNT_DEACTIVATED when the account is DEACTIVATED; 500 CUSTOMER_ACCOUNT_LOOKUP_FAILED when the lookup fails or the status is missing or unrecognised. All use ErrorResponseCUSTOMER_ACCOUNT_NOT_FOUND, CUSTOMER_ACCOUNT_DEACTIVATED and CUSTOMER_ACCOUNT_LOOKUP_FAILED to error code enumsystem-integrator/payout, which was never a real Cognito scope. Code and Cognito already enforce wallet-core/sfec:payout:create (POST) and wallet-core/sfec:payout:enquire (GET); this only corrects the docs to match.application/x-www-form-urlencoded with payload field (JWE compact token); all responses use Content-Type: application/jose (JWE compact token)JWE_DECRYPT_FAILED to error code enum — returned as plain JSON (400) when payload field is missing or JWE decryption failsPayoutStatusData (GET response): added completedAt, error, and pollAfterSeconds — now matches PayoutData (POST response) for symmetryPayoutError component schema: added as a named schema (shared by PayoutData.error and PayoutStatusData.error)completedAt description corrected: only set when payoutStatus = success (not on failure)CAMPAIGN_MISMATCH to error code enum — implementation raises this when the earmark's campaign does not match the requestproxyType enum — now accepts both short form (U/M/N) and long form (UEN/MOBILE/NRIC)customerId description — corrected to "SFEC customer ID. For SFEC, this is the company UEN (same as proxyValue)"processing (saga still running) and pending_settlement (OCBC settling), and reference pollAfterSecondspayoutStatus enum — expanded from ["success", "pending_settlement"] to full set ["processing", "pending_settlement", "success", "failed"]PAYOUT_IN_PROGRESS → PAYOUT_EXISTS — matches what the implementation actually returnscompletedAt from PayoutStep objects — field does not exist at step levelRuns the payout saga: transaction lookup → budget check → journal → earmark cancel → PayNow → reverse journal → bank transaction.
Requires a prior EARMARK_CREATE via Collections. sfec verifies proxyValue and amount against the transaction record, and on every request checks that the customer's wallet account exists and is not DEACTIVATED. The E1 earmark is cancelled before PayNow fires (Option B), releasing the held balance so the PayNow transfer can proceed.
| Idempotency-Key required | string <uuid> <= 64 characters Unique key per payout attempt (UUID recommended). Same key resumes the existing saga. |
JWE-encrypted request. Send as application/x-www-form-urlencoded with a single field payload containing the JWE compact serialization. The JWE plaintext is the JSON body described by PayoutRequest.
| payload required | string JWE compact serialization (5 base64url-encoded parts separated by dots). Decrypt to obtain the PayoutRequest JSON body. Algorithm: RSA-OAEP-512, Encryption: A256GCM. |
The payload field value is the JWE compact token. When decrypted, the plaintext JSON is:
payload=eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIn0.%3C...%3E.iv.ciphertext.tag
{ "data": { "payoutStatus": "success", "govwalletTransactionId": "6f1c2e8a-4b3d-4c9a-9e2f-1a7b8c9d0e1f", "customerId": "52871152X", "campaign": "rsfec-stg", "proxyType": "U", "proxyValue": "52871152X", "amount": 10000, "description": "wis", "currency": "SGD", "references": { "workdayJournalId": "wis-JRNL", "workdayReversalJournalId": "wis-RJRNL", "workdayBankTransactionId": "wis-BTXN", "paynowTxnId": "govwallet-paynow-txn-001" }, "completedAt": "2026-07-09T08:41:22Z" }, "meta": { "requestId": "b2f0a1d4-7c8e-4f2a-9b3c-5d6e7f8a9b0c", "timestamp": "2026-07-09T08:41:22Z" } }
Current saga state. Poll after 202 pending_settlement, or use for reconciliation.
The request has no body and is not JWE-encrypted. Do not send a payload parameter. SFEC ignores it. The response is JWE-encrypted to your registered public key, except 401/403.
| govwalletTransactionId required | string <uuid> The govwalletTransactionId from the original payout request (UUID from Collections earmark). |
| campaign required | string Campaign identifier. Must match the campaign used in the original payout request. Returns 404 if mismatched (prevents cross-campaign information leakage). |
{ "data": { "payoutStatus": "pending_settlement", "govwalletTransactionId": "6f1c2e8a-4b3d-4c9a-9e2f-1a7b8c9d0e1f", "customerId": "52871152X", "campaign": "rsfec-stg", "proxyType": "U", "proxyValue": "52871152X", "amount": 10000, "description": "wis", "currency": "SGD", "steps": [ { "step": "TRANSACTION_LOOKUP", "status": "completed" }, { "step": "BUDGET_CHECK", "status": "completed" }, { "step": "SUBMIT_JOURNAL", "status": "completed", "reference": "wis-JRNL" }, { "step": "EARMARK_CANCEL", "status": "completed" }, { "step": "PAYNOW_PAYOUT", "status": "pending", "reference": "wis" }, { "step": "REVERSE_JOURNAL", "status": "not_started" }, { "step": "ADHOC_BANK_TRANSACTION", "status": "not_started" } ], "updatedAt": "2026-07-09T08:14:05Z" }, "meta": { "requestId": "c3d1b2e5-8d9f-4a3b-8c4d-6e7f8a9b0c1d", "timestamp": "2026-07-09T08:14:05Z" } }
Ask Spoon