Debit Cards
Adding a Card
| Endpoint | Behavior |
|---|---|
|
Runs the Card-Change Rules (v1) ( |
|
Suspended-card check ( |
|
The v2 path without the floats and loans check, activation, or recalculation. |
The v2 floats and loans check runs only when the new card is primary and the user already has a valid primary card; a user without a valid primary card can always add one. A usio_card_id in the request is stored at creation; cards created without one receive it from the first pinless attempt — After submission.
Source: pkg/api/debit_cards.go, pkg/api/debit_cards_v2.go
Card-Change Rules (v1)
GET /{user_id}/debit-card/check and the v1 save evaluate these rules in order, stopping at the first decisive one. A support override on the latest change-history row (is_override = true) allows the change outright.
| Rule | Condition | Result |
|---|---|---|
Fraud window |
The user has prior change rows, joined less than |
User banned (User Service); evaluation continues |
No usable card |
No primary card, or the primary card is invalid |
Allowed |
Open floats |
Float Service reports active floats |
|
Open loans |
LOC Service reports an |
|
Change buffer |
|
Allowed; otherwise |
Recent bank change |
A Transactions Service item was created within ± |
Allowed |
Source: pkg/api/debit_cards.go (checkCanChangeCard), deploy/lambda.tf, deploy/variables.tf
Suspended Cards
suspended_on is set by the return-code engine on a suspend-flagged decline, or by an admin through PATCH /admin/{user_id}/payments/debit-cards/{debit_card_id}.
-
Adding a card with a
usio_card_idqueries GSI2 for every record with that ID, across all users and statuses. Any record withsuspended_onset rejects the add with409CARD_SUSPENDEDand writes an admin note on the user (Admin API, best-effort). Cards without ausio_card_idskip the check. -
Card responses report
is_valid = falsewhilesuspended_onis set; the stored validity is unchanged. -
Pinless debits on a suspended card are blocked before submission — Pinless gates.
-
The admin
PATCHsetssuspended_onfrom a timestamp, clears it from an empty string, and leaves it unchanged when omitted. Clearing one record does not make the physical card addable while another record still carries the attribute.
Change History
Rows in pinless-changes (schema) are snapshots of the card after a change, returned by GET /{user_id}/debit-card/changes.
| Written on | is_override |
|---|---|
Card added (v1, v2, prequalify) |
|
|
|
|
|
|
|
Ordinary deletes write nothing.
Removal
DELETE /{user_id}/debit-card, POST /{user_id}/debit-card/force, and DELETE /{user_id}/payments/debit-cards/{debit_card_id} set status = REMOVED on the item and delete the pinless-default-card row when the card was primary ALL. The two v1 paths remove every ACTIVE card the user holds; the v2 path removes the one card.
First-Debit Event
A card save (v1 or v2) sends first_debit_card_connected to Segment and AppsFlyer (server-to-server, per platform) once per user lifecycle:
-
Look up the user (User Service) for
appsflyer_id,app_platformandfirebase_instance_id; the request’s platform and AppsFlyer headers are fallbacks. -
Acquire the
first_debit_card_connectedmarketing lock. Already held → skip. -
Send to Segment with
appsflyer_id,app_platformand the Firebase app instance ID asfirebase_install_id(sent even when blank), then AppsFlyer. A Segment failure, or a lock error other than a timeout, releases the lock so the next card save retries; AppsFlyer errors and unknown platforms keep it.
In test the event is named first_debit_card_connected_test.
Source: pkg/api/first_debit_notifier.go
Token Refresh
-
site-payments-usio-refresh-scheduler(daily 15:00 UTC) queries cards expiring this month: GSI1DEBITCARDEXP#<MMYYYY>on the single table pluscard_exponpinless-default-card, de-duplicated by credit + debit token pair and excludingSTALEcards. One SQS message per card. -
site-payments-usio-refresh-workersubmits refresh requests for the card’s credit and debit tokens against themainUsio credentials and stores a refresh crumb holding both confirmation IDs. -
For a card that already has a crumb, the worker fetches the results instead: both succeeded →
expiration_dateis updated (the tokens do not change); a failure →status = STALE; still processing → left for the next run.
Source: pkg/usio/refresher/, pkg/dynamo/debit_card.go (GetExpiring)
Admin and QA Endpoints
| Endpoint | Effect |
|---|---|
|
|
|
Sets |
|
Swap the card’s Usio tokens (QA failure injection) |
|
Retry-limit ledger view — Retry limit |
Source: pkg/api/admin.go, pkg/api/qa_backend.go, pkg/api/retry_limit.go
Debit Fixer
scripts/debit-fixer reads test_users.csv and enqueues {"user_id": …} messages to site-payments-debit-fixer. For each user, site-payments-debit-card-fixer loads the ACTIVE cards, removes all but one, and re-saves the remaining card as primary ALL when its GSI1 key lacks the DEBITCARDEXP# prefix.
Source: pkg/debitfixer/debit_fixer.go, scripts/debit-fixer/main.go