Payment Syncing
ACH payments are saved as ACHSENT and resolved later. Four mechanisms write the resolution to the payments table; every write that sets an event_type is published to Kinesis (Events).
| Mechanism | Trigger | Resolves |
|---|---|---|
JPM callback |
JPM |
|
Daily 17:00 UTC |
|
|
Every 30 min; daily 16:30 and 20:30 UTC |
Usio returns → |
|
Daily 13:00 UTC |
JPM transaction details to |
JPM Webhooks
JPM posts status changes to POST /v1/jpm on jpm.webhooks.floatme.io (spec spec/jpm_webhook_api.yaml, rendered at API Spec).
site-payments-jpm-webhook-handler:
-
Rejects a payload missing
messageIdormessageTypewith422. -
Enqueues each transaction whose
paymentStatusisREJECTEDorRETURNEDto SQSsite-payments-jpm-webhooksas anfmsdk.Event; every other status is acknowledged with200and dropped. -
Returns
500when any enqueue fails, so JPM redelivers the whole webhook.
site-payments-jpm-webhook-processor, per queued transaction:
-
Loads the payment by
endToEndId(=confirmation_id). -
Skips a payment already
FAILED. -
Sets
payment_status = FAILED,return_code=processingInfo.statusCode,return_info=processingInfo.statusText, andevent_type=PRENOTE_RETURNEDfor a prenote, otherwiseSUBSCRIPTION_RETURNED.
The processor stores JPM’s codes untranslated; the return-code engine normalizes them.
Source: pkg/jpm/webhooks/webhook_server.go, pkg/jpm/webhooks/webhook_processor.go
Check ACH
site-payments-check-ach-scheduler (EventBridge check-ach, 17:00 UTC) queries payments on payment_status-index for ACHSENT records with payment_type != credit and submit_date before now, and fans them out to SQS site-payments-check-ach across 20 parallel SendMessageBatch workers of 1,000 entries each. There is no age cutoff: every open debit is enqueued daily.
site-payments-check-ach-worker, per payment:
-
Plaid clearing — a float debit matched in the user’s Plaid transactions is
COMPLETEDwithFLOAT_DEBIT_COMPLETED_PLAID. -
Otherwise, once the 3-business-day gate has passed:
-
JPM —
GET /tsapi/v3/payment/{id};COMPLETEDunless JPM reportsREJECTEDorRETURNED, which are left to the webhook path. -
Usio —
COMPLETEDwithout a provider call; returns were written earlier the same day by the Usio ACH-returns sync.
-
-
event_typeis set by payment class; a class that cannot be determined fails the record without a write.
| Payment class | event_type on completion |
|---|---|
Subscription ( |
|
Float debit ( |
|
Float credit ( |
|
Loan debit ( |
|
Loan credit |
|
Prenote |
|
The queue’s maxReceiveCount = 1 sends a failed record straight to the DLQ.
Source: pkg/checkach/scheduler.go, pkg/checkach/worker.go
Plaid clearing
All gates must hold; otherwise the payment waits for the provider path:
-
The payment is a debit with a
loan_id. -
The user has at least one
COMPLETEDfloat (rank ≥ 1). First-time floaters are never Plaid-cleared. -
floats.collections.day_before.returning_float_achis off for the user (GrowthBook Flags). -
The float’s collection history (Float Service) holds exactly one entry, the in-flight
ACHSENTattempt.
The match is a settled FloatMe debit for the payment amount among the user’s Transactions Service transactions from two days before submit_date onward.
3-business-day gate
The provider path runs once three business days have elapsed after submit_date:
-
A
submit_dateat or after 21:00 America/Chicago counts from the next calendar day. -
Business days exclude Saturday, Sunday, and US bank holidays (
pkg/businessdays). -
Days are counted strictly after the (possibly shifted) submit day up to and including today, both truncated to midnight UTC.
Source: pkg/checkach/worker.go, pkg/businessdays/businessdays.go
Usio Sync
Usio has no webhooks. site-payments-usio-scheduler receives one of three EventBridge inputs and enqueues one SQS message per merchant account to site-payments-usio-sync; site-payments-usio-syncer (concurrency 1) runs each job.
| Rule | Input | Window | Job |
|---|---|---|---|
|
|
Rolling 35 min |
New transactions → staging tables |
|
|
Today, America/Chicago |
ACH returns → |
|
|
Today, America/Chicago |
Chargebacks → |
All three rules are prod-only (schedules). Accounts synced: float-debit, float-credit, subscription, subscription-2, main (tokenization), loan-credit, loan-debit; subscription-2 is skipped for ach_returns. A manual invocation with {"days": N} and no job_type runs all three sub-syncs per day per account; ENABLE_REFUND (false in Terraform) would add a tokenization-refund job to those runs.
Per job:
-
transactions — every transaction in the window is written to
usio-debit-transactions/usio-credit-transactions, or tosite-usio-tokenization-transactionsfor themainaccount (schema). -
ach_returns — for each
achreturntransaction whose confirmation matches a payment not alreadyFAILED:payment_status = FAILED,return_code= UsioFinalStatus,return_info=ErrorMessage,event_typeby class (SUBSCRIPTION_RETURNED,FLOAT_DEBIT_RETURNED,FLOAT_CREDIT_RETURNED,LOAN_DEBIT_RETURNED,LOAN_CREDIT_RETURNED,PRENOTE_RETURNED). Raw rows also go to the staging tables. -
chargebacks — for each
dbchbacktransaction with a matching payment:payment_status = CHARGED_BACK,event_typeSUBSCRIPTION_CHARGED_BACK,FLOAT_DEBIT_CHARGED_BACK, orLOAN_DEBIT_CHARGED_BACK.
Source: pkg/usio/syncer/scheduler.go, pkg/usio/syncer/syncer.go
ACH risk report
The ach_returns job also calls Usio GetACHRiskReport for each account it syncs and stores one USIOACHRISK# item per merchant per Chicago calendar day (schema): return counts and rates by return date and by submit date. A same-day re-run overwrites the item. Nothing in this service reads the items back.
Usio’s per-merchant thresholds these snapshots track (Usio ACH return codes):
| Rate | Threshold | Codes |
|---|---|---|
Overall |
< 15% |
All |
Administrative |
< 3% |
|
Unauthorized |
< 0.5% |
|
JPM Transaction Sync
site-payments-jpm-syncer (EventBridge jpm-syncer, 13:00 UTC) fetches JPM transaction details for the three days ending now and writes each to jpm-transaction-details (schema). It does not touch payment records.
Source: pkg/jpm/syncer/syncer.go