Payment Syncing
ACH payments do not settle instantly. Three mechanisms keep payment records in DynamoDB in sync with the actual state at the payment processors: JPM webhooks, daily ACH status polling, and Usio transaction syncing. Each mechanism updates the same prod-payments table.
See Payment Flow for the initial submission lifecycle.
JPM Webhooks
JPM sends a status callback to webhooks.floatme.io whenever a payment’s status changes. The webhook handler decouples receipt from processing by immediately enqueuing the event to SQS.
JPM │ POST /v1/jpm (status change callback) ▼ API Gateway (webhooks.floatme.io) │ ▼ jpm-webhook-handler (Lambda) │ enqueue raw payload (visibility_timeout = 600 s) ▼ SQS (jpm-webhooks) │ trigger ▼ jpm-webhook-processor (Lambda) │ look up payment by JPM confirmation ID │ update payment_status in DynamoDB │ if RETURNED or REJECTED: write return_code ▼ DynamoDB (prod-payments) │ ▼ kinesis-feeder (DynamoDB Streams → Kinesis) │ publish event_type to prod-payments stream ▼ blocklist-handler (if SUBSCRIPTION_RETURNED or FLOAT_DEBIT_RETURNED)
The jpm-webhook-processor looks up the payment record by JPM’s endToEndId (stored as confirmation_id), then writes the updated status. If the status is RETURNED or REJECTED, the return_code field is also set, which the downstream Kinesis pipeline uses to determine blocklist action.
See JPM Integration for the full JPM status set and return code mappings.
Check ACH (Daily Polling)
Not all ACH status changes arrive via webhook. The check-ach pipeline polls both JPM and Usio daily for outstanding ACHSENT records and resolves any that have settled.
CloudWatch EventBridge (15:30 UTC daily)
│
▼
check-ach-scheduler (Lambda)
│ query DynamoDB for all ACHSENT payments
│ enqueue one SQS message per payment
▼
SQS (check-ach, visibility_timeout = 60 s, maxReceive = 1)
│ trigger (batch_size = 10)
▼
check-ach-worker (Lambda)
│ per payment: call provider status API
│ ├─ JPM: GET /tsapi/v3/payment/{id}
│ └─ Usio: query by confirmation_id
│ if status changed: write updated payment record
▼
DynamoDB (prod-payments)
The check-ach queue uses maxReceiveCount = 1, so a single worker failure sends the message to the DLQ without retry. This prevents duplicate status writes for the same payment.
check-ach-worker also notifies the Transaction Service when a float payment transitions out of ACHSENT, so the transaction record can be updated accordingly.
Usio Sync
Usio has no webhook mechanism. A separate sync pipeline polls Usio transaction data every 30 minutes and reconciles it against DynamoDB records.
CloudWatch EventBridge (every 30 min, prod only)
│ input: {"minutes": 35}
▼
usio-scheduler (Lambda)
│ enqueue one SQS message per merchant account per date window
▼
SQS (usio-sync, visibility_timeout = 600 s)
│ trigger (batch_size = 1)
▼
usio-syncer (Lambda)
│ fetch transaction list from Usio API for account + date range
│ for each transaction: match to existing payment record
│ ├─ if match found: update status + return_code on prod-payments
│ └─ always: write the raw transaction to the Usio staging table
│ (usio-debit-transactions or usio-credit-transactions
│ depending on transaction type)
▼
DynamoDB (prod-payments, usio-debit-transactions, usio-credit-transactions)
The Usio sync covers all four merchant accounts (subscription debit, float debit, float credit, loan accounts). It is the primary reconciliation mechanism for all Usio ACH payments since Usio does not send webhooks.
Status Update Outcomes
When a syncing mechanism determines that an ACHSENT payment has settled, it writes the updated payment record. The outcome depends on the new status:
| New Status | Trigger Condition | Downstream Effect |
|---|---|---|
|
JPM: status = |
Payment record updated. |
|
JPM: status = |
Payment record updated with |
|
JPM: status = |
Payment record updated with |
|
JPM: status = |
Intermediate status. Payment remains in progress. No blocklist action. |
Usio Token Refresh
Usio debit/credit tokens can expire. A separate daily job refreshes tokens before they become invalid:
CloudWatch EventBridge (15:00 UTC daily) │ ▼ usio-refresh-scheduler (Lambda) │ query DynamoDB for cards with expiring tokens │ enqueue one SQS message per card ▼ SQS (usio-refresh, visibility_timeout = 60 s, maxReceive = 1) │ trigger (batch_size = 10) ▼ usio-refresh-worker (Lambda) │ call Usio token refresh API │ update token in DynamoDB (new table + legacy table) ▼ DynamoDB (prod-payments single table + pinless-default-card)
The scheduler queries both the new prod-payments single table and the legacy pinless-default-card table for expiring cards, de-duplicating results by token reference ID pair before enqueuing. The worker writes the refreshed token to both tables.
|
JPM Transaction Sync
In addition to webhook callbacks, a daily job syncs JPM raw transaction details to a separate DynamoDB staging table:
CloudWatch EventBridge (13:00 UTC daily) │ ▼ jpm-syncer (Lambda) │ fetch transaction detail records from JPM API │ write to DynamoDB (jpm transaction details table)
This sync stores raw JPM transaction data for audit and reconciliation purposes, separate from the payment status updates handled by the webhook pipeline.
Related Pages
-
Payment Flow — Initial payment submission and status lifecycle
-
JPM Integration — JPM ACH, webhook callbacks, and return code mappings
-
Usio Integration — Usio ACH, pinless, and merchant account details
-
Blocklist — How returned payment events trigger automatic user blocklisting
-
Event Flows — Kinesis event types published after status updates