Skip to content

Transaction Lifecycle & State Machine Standard

Version: 1.0 Last Updated: 2026-04-03 Owner: Platform Team Status: Active

Purpose

Standardise transaction states, transitions, and naming across all Simpaisa products. Replace legacy numeric status codes with human-readable, consistent state names. Ensure every state transition fires a webhook.

Conventions

  • All state names: UPPERCASE_SNAKE_CASE
  • States are either terminal (no further transitions) or non-terminal (transitions expected)
  • Every transition MUST be logged with: timestamp, actor (system/user/operator), reason
  • Every transition MUST fire a webhook event to the merchant (no silent transitions)

Pay-In States

INITIATED → PENDING_OTP → OTP_VERIFIED → PROCESSING → COMPLETED
                                                     → FAILED
                                       → EXPIRED
State Terminal Trigger Webhook Event
INITIATED No Merchant submits pay-in request payin.initiated
PENDING_OTP No OTP sent to customer MSISDN payin.otp_sent
OTP_VERIFIED No Customer submits correct OTP payin.otp_verified
PROCESSING No Request forwarded to channel payin.processing
COMPLETED Yes Channel confirms debit payin.completed
FAILED Yes Channel rejects or business rule violation payin.failed
EXPIRED Yes OTP or session timeout (5 min default) payin.expired

Allowed transitions: - INITIATEDPENDING_OTP, FAILED, EXPIRED - PENDING_OTPOTP_VERIFIED, FAILED, EXPIRED - OTP_VERIFIEDPROCESSING, FAILED - PROCESSINGCOMPLETED, FAILED

Pay-Out States

PUBLISHED → IN_REVIEW → PROCESSING → DISBURSED
                                    → REJECTED
                      → ON_HOLD → STUCK → REJECTED
                                        → PROCESSING
                                → PROCESSING
                      → REVERSED
State Terminal Trigger Webhook Event
PUBLISHED No Merchant submits pay-out request payout.published
IN_REVIEW No Compliance/fraud check triggered payout.in_review
PROCESSING No Sent to channel for disbursement payout.processing
ON_HOLD No Manual review required (amount threshold, AML flag) payout.on_hold
STUCK No Channel returned ambiguous response; requires investigation payout.stuck
DISBURSED Yes Channel confirms credit to beneficiary payout.disbursed
REJECTED Yes Compliance rejection or channel decline payout.rejected
REVERSED Yes Funds returned after disbursement failure payout.reversed

Allowed transitions: - PUBLISHEDIN_REVIEW, PROCESSING, REJECTED - IN_REVIEWPROCESSING, ON_HOLD, REJECTED - PROCESSINGDISBURSED, STUCK, REJECTED - ON_HOLDPROCESSING, STUCK, REJECTED - STUCKPROCESSING, REJECTED - DISBURSEDREVERSED

Remittance States

PUBLISHED → IN_PROCESS → IN_REVIEW → ON_HOLD → AML_REVIEW → REMITTED
                                                            → REJECTED → REVERSED
State Terminal Trigger Webhook Event
PUBLISHED No Remittance partner submits transaction remittance.published
IN_PROCESS No Initial validation and routing remittance.in_process
IN_REVIEW No Compliance screening triggered remittance.in_review
ON_HOLD No Additional documentation required remittance.on_hold
AML_REVIEW No Escalated to AML team remittance.aml_review
REMITTED Yes Funds credited to beneficiary remittance.remitted
REJECTED Yes Compliance rejection or channel failure remittance.rejected
REVERSED Yes Funds returned to sending corridor remittance.reversed

Webhook gap fix: Legacy system only fires webhooks for PUBLISHED, IN_PROCESS, REMITTED, and REJECTED. The v3 implementation MUST fire webhooks for ALL states including IN_REVIEW, ON_HOLD, AML_REVIEW, and REVERSED.

Allowed transitions: - PUBLISHEDIN_PROCESS, REJECTED - IN_PROCESSIN_REVIEW, REMITTED, REJECTED - IN_REVIEWON_HOLD, AML_REVIEW, REMITTED, REJECTED - ON_HOLDIN_REVIEW, AML_REVIEW, REJECTED - AML_REVIEWREMITTED, REJECTED - REJECTEDREVERSED

Card States

INITIATED → AUTHORISED → CAPTURED → REFUNDED (partial/full)
                       → VOIDED
State Terminal Trigger Webhook Event
INITIATED No Card payment request received card.initiated
AUTHORISED No Issuer approves the authorisation card.authorised
CAPTURED Yes* Merchant captures the authorised amount card.captured
VOIDED Yes Merchant voids before capture card.voided
REFUNDED Yes Merchant issues full or partial refund card.refunded

*CAPTURED transitions to REFUNDED if a refund is issued.

Allowed transitions: - INITIATEDAUTHORISED, FAILED - AUTHORISEDCAPTURED, VOIDED - CAPTUREDREFUNDED

Partial refunds: Multiple REFUNDED events may fire. Each includes refundAmount and remainingAmount. The transaction reaches terminal state when remainingAmount equals zero.

State Transition Audit Trail

Every state change MUST produce an audit record in SurrealDB:

{
  "transactionId": "TXN-20260403-00042",
  "previousState": "PROCESSING",
  "newState": "COMPLETED",
  "timestamp": "2026-04-03T14:22:01.123Z",
  "actor": "system:payin-svc",
  "reason": "Channel confirmed debit",
  "traceId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
}

Actor format: system:<service-name>, operator:<email>, or api:<merchant-id>.

Idempotency Interaction

Scenario Behaviour
Duplicate request, same idempotency key, same body Return current state (no new transition)
Duplicate request, same idempotency key, different body Return 409 Conflict
Transaction in terminal state, retry received Return terminal state, no reprocessing
Transaction in PROCESSING, retry received Return PROCESSING status, do not re-submit to channel
Transaction in STUCK, retry received Allow re-submission to channel (operator-initiated only)

Legacy Code Mapping

For backward compatibility, the v3 API returns both the new state name and the legacy numeric code in the response body. The legacyStatusCode field is deprecated and will be removed in v4.

{
  "transactionStatus": "COMPLETED",
  "legacyStatusCode": 2
}