Payment Flow
End-to-end lifecycle of a payment — from API submission through provider routing, DynamoDB persistence, and final settlement. See Payment Syncing for how ACH statuses are resolved after initial submission.
Payment Types
The SubmitPayment endpoint routes each request to one of five paths based on the combination of request fields and provider:
| Payment Type | Direction | Provider | Method | Initial Status |
|---|---|---|---|---|
Subscription ACH |
Debit (collection) |
JPM (default) or Usio (override) |
ACH bank transfer |
|
Float / loan ACH credit |
Credit (disbursement) |
Usio |
ACH bank transfer |
|
Float / loan ACH debit |
Debit (collection) |
Usio |
ACH bank transfer |
|
Pinless credit |
Credit (disbursement) |
Usio |
Tokenized card |
|
Pinless debit |
Debit (collection) |
Usio |
Tokenized card |
|
RTP credit |
Credit (disbursement) |
Usio |
Real-Time Payments |
|
RTP is attempted when the caller sets rtp_mode on a credit request. If the routing number is not RTP-eligible and rtp_mode = fallback, the payment falls back to ACH automatically. If rtp_mode = only, a failure is returned.
|
Submission Flow
ACH Payments
Caller
│ POST /{user_id}/payments
▼
Payments API (Lambda)
│ decode request body
│ route: debit or credit?
│
├─ credit + ach_information ──► check rtp_mode?
│ ├─ RTP mode? ──► check RTP eligibility (Usio)
│ │ ├─ eligible ──► submit RTP credit (Usio)
│ │ └─ not eligible (if fallback) ──► fall through to ACH
│ └─ no RTP ──► submit ACH credit (Usio)
│
└─ debit + ach_information ──► subscription?
├─ yes + no provider override ──► submit ACH debit (JPM)
└─ yes + provider override ──► submit ACH debit (Usio)
float/loan debit ──► submit ACH debit (Usio)
│
▼
Provider ACH API (JPM or Usio)
│ accepts payment, returns confirmation ID
▼
Payments API
│ build payment record (status = ACHSENT)
│ save to DynamoDB (prod-payments)
│ return confirmation_id + status to caller
Pinless Payments
Caller
│ POST /{user_id}/payments
▼
Payments API (Lambda)
│ decode request body
│ route: debit or credit?
│
├─ credit (no ach_information) ──► fetch credit token from DynamoDB
└─ debit (no ach_information) ──► fetch debit token from DynamoDB
│
▼
Usio Pinless API (tokenized)
│ success → confirmation ID
│ failure → error message (no confirmation ID)
▼
Payments API
│ on success: build payment record (status = COMPLETED)
│ save to DynamoDB (prod-payments)
│ on failure: return FAILED (no DynamoDB write — no confirmation ID)
│ return confirmation_id + status to caller
| Usio does not return a confirmation ID on failure for either ACH or pinless payments. Failed payments are not persisted to DynamoDB. |
Payment Status Transitions
ACH payments are asynchronous. The initial status after submission is ACHSENT, and the final status is determined later through webhook callbacks or daily polling.
┌─────────────┐
│ SUBMITTED │ (API submits to provider)
└──────┬──────┘
│
┌────────▼────────┐
│ ACHSENT │ saved to DynamoDB immediately after submission
└────────┬────────┘
│
┌──────────────┴──────────────────┐
│ │
┌────▼────┐ ┌──────────▼─────────────────┐
│COMPLETED│ │ FAILED (return_code=XYZ) │
└─────────┘ └────────────────────────────┘
(JPM: cleared, (JPM: rejected; Usio failed;
Usio: settled) bank returned with NACHA or
JPM return code — return_code
and return_info are set)
Pinless / RTP:
SUBMITTED → COMPLETED (immediate, no ACHSENT step)
SUBMITTED → FAILED (immediate, no DynamoDB write)
Returned ACH payments transition to FAILED with return_code (and return_info) populated — there is no distinct RETURNED payment_status. The return_code distinguishes a rejection (e.g. AC01) from a bank return (e.g. R02 / AC04); the event_type on the record (SUBSCRIPTION_RETURNED, FLOAT_DEBIT_RETURNED, etc.) tells downstream Kinesis consumers which class of return occurred.
Status is written by one of three mechanisms:
-
JPM webhook:
jpm-webhook-processorupdatespayment_statuswhen JPM sends a callback. -
Check ACH polling:
check-ach-workerpolls both JPM and Usio daily for outstandingACHSENTrecords. -
Usio sync:
usio-syncerfetches Usio transaction data every 30 minutes and reconciles payment records.
See Payment Syncing for details on all three mechanisms.
Pinless vs ACH
| Attribute | Pinless / RTP | ACH |
|---|---|---|
Settlement timing |
Instant (synchronous response) |
Several business days |
Initial status |
|
|
Final status reconciliation |
Not required — status is final at submission |
Required — via webhook, polling, or Usio sync |
Confirmation ID on failure |
Not returned by Usio |
Not returned by Usio |
Blocklist trigger |
Not applicable |
Return codes R02/R03/R04/R16 (or JPM equivalents) |
Kinesis Event
After a successful payment is saved to DynamoDB, the kinesis-feeder Lambda reads the DynamoDB Streams record and publishes an fmsdk.Event to the prod-payments Kinesis stream. The event type is taken from the payment’s event_type field.
Payments submitted through the API have an empty event_type at write time. The event type is set later when the payment status is updated (e.g., to SUBSCRIPTION_RETURNED or FLOAT_DEBIT_RETURNED). Only records with a non-empty event_type are forwarded to Kinesis.
See Event Flows for the full list of event types and their consumers.
Error Handling
| Scenario | Behavior |
|---|---|
Provider API error (network / 5xx) |
|
Provider rejects payment (validation failure) |
|
DynamoDB write failure after successful provider submission |
|
Pinless token not found ( |
Returned as a failure response. Token may have been created under the old master account and not migrated. See Usio Integration. |
Invalid debit card (non-allowlisted failure code) |
Debit card is automatically marked |
Related Pages
-
Payment Syncing — How ACH payment statuses are resolved after submission
-
JPM Integration — JPM ACH details, webhook flow, and return codes
-
Usio Integration — Usio ACH, pinless, and RTP details
-
Blocklist — How returned payments trigger automatic blocklisting
-
Event Flows — Kinesis events published for each payment lifecycle transition