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 resolves outstanding ACHSENT records daily: JPM payments are polled against JPM’s status API, while Usio payments are auto-completed once they’ve aged past the 3-business-day gate (returns are handled separately by the daily Usio ACH-returns sweep that runs 30 minutes earlier).

CloudWatch EventBridge (17:00 UTC daily, 30 min after Usio ACH returns sync)
 │
 ▼
check-ach-scheduler (Lambda)
 │  query DynamoDB for ACHSENT payments, excluding credits
 │  (filter at source) — no submit_date cutoff
 │  fan out SQS SendMessageBatch across a worker pool
 ▼
SQS (check-ach, visibility_timeout = 60 s, maxReceive = 1)
 │  trigger (batch_size = 10)
 ▼
check-ach-worker (Lambda)
 │  per payment, once past the 3-business-day gate:
 │    ├─ JPM: GET /tsapi/v3/payment/{id} (mark COMPLETED unless RETURNED/REJECTED)
 │    └─ Usio: auto-flip to COMPLETED (returns already written by daily sweep)
 │  if status changed: write updated payment record
 ▼
DynamoDB (prod-payments)

The scheduler filters at the DynamoDB source on a single criterion so it doesn’t enqueue records the worker would early-return on:

  • payment_type != "credit" — credits are never cleared by this pipeline.

There is no longer a submit_date cutoff: every ACHSENT debit is enqueued regardless of age. The worker applies its own clearing gates (the plaid path clears as soon as a matching settled transaction appears; the JPM/Usio provider path still requires 3 business days), so age filtering is left entirely to the worker.

The enqueue phase fans SendMessageBatch calls across a small goroutine pool (sqsParallelism workers, each handling sqsChunkSize entries), because at peak volume a serial loop of ten-message batches exceeds the 15-minute Lambda timeout. fmsdk’s SendMessageBatch still sub-chunks each worker’s slice into AWS’s 10-entry batches internally.

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 and reconciles it against DynamoDB records. There are three CloudWatch schedules feeding the same usio-scheduler Lambda:

  • usio-transactions-syncer — every 30 min, job_type=transactions, 35-minute rolling window. Runs new transactions only.

  • usio-ach-returns-syncer — daily at 16:30 UTC (10:30 AM CST / 11:30 AM CDT, after Usio’s stated 10:00 AM CT return-drop cutoff), job_type=ach_returns, today’s Central calendar day window (Usio reports returns by Central calendar day). Runs only ACH returns. Split out because Usio publishes ACH returns once per day and asked us to stop polling achreturn when it returns nothing.

  • usio-chargebacks-syncer — daily at 20:30 UTC (2:30 PM CST / 3:30 PM CDT, after 2 PM CT), job_type=chargebacks, today’s Central calendar day window. Runs only chargebacks. Split out for the same reason as ACH returns: Usio publishes chargebacks once per day, so polling on the 30-min schedule was unnecessary.

CloudWatch EventBridge (three rules, prod only)
 │  transactions: every 30 min  → {"minutes": 35, "job_type": "transactions"}
 │  ach_returns:  daily 16:30 UTC → {"today": true, "job_type": "ach_returns"}
 │  chargebacks:  daily 20:30 UTC → {"today": true, "job_type": "chargebacks"}
 ▼
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)
 │  dispatch on job_type:
 │   ├─ transactions: new transactions only
 │   ├─ ach_returns:  ach returns only
 │   └─ chargebacks:  chargebacks only
 │  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

COMPLETED

JPM: status = COMPLETED; Usio: transaction settled successfully

Payment record updated. kinesis-feeder publishes event. No blocklist action.

FAILED (rejected)

JPM: status = REJECTED; Usio: immediate failure response

Payment record updated with return_code. kinesis-feeder publishes event. Blocklist evaluated.

FAILED (returned)

JPM: status = RETURNED; Usio: ACHSENT payment later appears as returned

Payment record updated with return_code. kinesis-feeder publishes event with type SUBSCRIPTION_RETURNED or FLOAT_DEBIT_RETURNED. Blocklist evaluated by blocklist-handler.

CLEARED

JPM: status = CLEARED (internal JPM review passed)

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.

  • 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