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

ACHSENT

Float / loan ACH credit

Credit (disbursement)

Usio

ACH bank transfer

ACHSENT

Float / loan ACH debit

Debit (collection)

Usio

ACH bank transfer

ACHSENT

Pinless credit

Credit (disbursement)

Usio

Tokenized card

COMPLETED

Pinless debit

Debit (collection)

Usio

Tokenized card

COMPLETED

RTP credit

Credit (disbursement)

Usio

Real-Time Payments

COMPLETED

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-processor updates payment_status when JPM sends a callback.

  • Check ACH polling: check-ach-worker polls both JPM and Usio daily for outstanding ACHSENT records.

  • Usio sync: usio-syncer fetches 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

COMPLETED (success) or FAILED (failure)

ACHSENT

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)

SubmitPayment returns HTTP 500. No DynamoDB write.

Provider rejects payment (validation failure)

SubmitPayment returns HTTP 200 with status = FAILED. No DynamoDB write if Usio (no confirmation ID returned). JPM rejections that have a confirmation ID are saved.

DynamoDB write failure after successful provider submission

SubmitPayment returns HTTP 500. Payment was submitted to the provider but is not tracked in DynamoDB. Requires manual reconciliation.

Pinless token not found (5030: Unable to locate record)

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 is_valid = false in DynamoDB to prevent future use.

  • 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