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 Webhooks

JPM callback

JPM REJECTED / RETURNED → FAILED

Check ACH

Daily 17:00 UTC

ACHSENT debits → COMPLETED by Plaid match, JPM poll, or the 3-business-day rule

Usio Sync

Every 30 min; daily 16:30 and 20:30 UTC

Usio returns → FAILED; chargebacks → CHARGED_BACK; raw transactions to staging tables; ACH risk report

JPM Transaction Sync

Daily 13:00 UTC

JPM transaction details to jpm-transaction-details; no payment writes

system context payment syncing

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:

  1. Rejects a payload missing messageId or messageType with 422.

  2. Enqueues each transaction whose paymentStatus is REJECTED or RETURNED to SQS site-payments-jpm-webhooks as an fmsdk.Event; every other status is acknowledged with 200 and dropped.

  3. Returns 500 when any enqueue fails, so JPM redelivers the whole webhook.

site-payments-jpm-webhook-processor, per queued transaction:

  1. Loads the payment by endToEndId (= confirmation_id).

  2. Skips a payment already FAILED.

  3. Sets payment_status = FAILED, return_code = processingInfo.statusCode, return_info = processingInfo.statusText, and event_type = PRENOTE_RETURNED for a prenote, otherwise SUBSCRIPTION_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:

  1. Plaid clearing — a float debit matched in the user’s Plaid transactions is COMPLETED with FLOAT_DEBIT_COMPLETED_PLAID.

  2. Otherwise, once the 3-business-day gate has passed:

    • JPM — GET /tsapi/v3/payment/{id}; COMPLETED unless JPM reports REJECTED or RETURNED, which are left to the webhook path.

    • Usio — COMPLETED without a provider call; returns were written earlier the same day by the Usio ACH-returns sync.

  3. event_type is set by payment class; a class that cannot be determined fails the record without a write.

Payment class event_type on completion

Subscription (subscription_id or prorated_subscription_id)

SUBSCRIPTION_COMPLETED

Float debit (loan_id, payment_type = debit)

FLOAT_DEBIT_COMPLETED

Float credit (loan_id, payment_type = credit)

FLOAT_CREDIT_COMPLETED

Loan debit (installment_loan_id)

LOAN_DEBIT_COMPLETED

Loan credit

LOAN_CREDIT_COMPLETED

Prenote

PRENOTE_COMPLETED

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 COMPLETED float (rank ≥ 1). First-time floaters are never Plaid-cleared.

  • floats.collections.day_before.returning_float_ach is off for the user (GrowthBook Flags).

  • The float’s collection history (Float Service) holds exactly one entry, the in-flight ACHSENT attempt.

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_date at 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

usio-transactions-syncer (every 30 min)

{"minutes":35,"job_type":"transactions"}

Rolling 35 min

New transactions → staging tables

usio-ach-returns-syncer (16:30 UTC)

{"today":true,"job_type":"ach_returns"}

Today, America/Chicago

ACH returns → FAILED, then ACH risk report

usio-chargebacks-syncer (20:30 UTC)

{"today":true,"job_type":"chargebacks"}

Today, America/Chicago

Chargebacks → CHARGED_BACK

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 to site-usio-tokenization-transactions for the main account (schema).

  • ach_returns — for each achreturn transaction whose confirmation matches a payment not already FAILED: payment_status = FAILED, return_code = Usio FinalStatus, return_info = ErrorMessage, event_type by 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 dbchback transaction with a matching payment: payment_status = CHARGED_BACK, event_type SUBSCRIPTION_CHARGED_BACK, FLOAT_DEBIT_CHARGED_BACK, or LOAN_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%

R02, R03, R04

Unauthorized

< 0.5%

R05, R07, R10, R11, R29, R51

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