Card Provisioning
The Whole Lifecycle
Provisioning a card is two stages. bulkCardGen reads a template and creates the cards on Tribe, logging the new ids to a file. Those ids then feed bulkCardImport, which reads each card back from Tribe and mirrors it into the cpm database. Gen writes only to Tribe; import is what populates our DB.
Stage 1: bulkCardGen — Create on Tribe
The Input File — A 3-Section Template
One row per kind of card. ~70 columns mapping to Tribe's create-card params; a card_count column says how many to mint from that row.
request_param,default_value pairs. Any column a row leaves blank falls back here. Unknown param names are rejected.
Named, reusable address blocks. A row references one via holder_address / delivery_address / bulk_address instead of repeating fields.
A Sample Template
# comments start with # [bulk_card_create] card_program_id,card_design_id,holder_address,card_count 380107,55,uk_office,500 380107,55,uk_office,250 [create_card_defaults] request_param,default_value card_country_ison,826 card_virtual,0 default_locale,en_GB [address_templates] template_name,city,zipcode,country_ison,address_line_1 uk_office,London,EC1A 1BB,826,1 High St
How Each Field Is Resolved
Flow — What Happens Per Card
build resolve params → CreateCardAction
Pull each column for this card (row → defaults → address template), normalise ISO codes, and assemble the Tribe CreateCardAction request.
gate ≤6 worker pool
Card #1 runs synchronously (fail-fast — bad creds/program surface before the whole batch fires); the rest fan out across the pool, capped at 6 concurrent for Tribe.
tribe · RSA createCard — or rundry
new Cards(api).createCard(req) creates the card on Tribe and returns its ids. -rundry true fabricates a response and skips Tribe — for validating a template safely.
check error handling
A Tribe rejection is counted; with -maxErrorCount at its default 0, the first error stops the batch. Pass a higher value to push through.
record notifier writes to out.csv
A single notifier thread appends the result — cardId,cardExtId,programId,designId,… on success, or seq,rootCause on failure. Nothing is written to cpm here — cards live on Tribe; import mirrors them in (stage 2).
The Hand-off
Gen's output rows are exactly the ids import needs — Tribe's card id becomes import's spiCardId; the external ref carries over.
1499713,9295121628797499,380107,55,1,…
1499713,9295121628797499,
Stage 2: bulkCardImport — Mirror into cpm
The Input File — A Flat Card List
A Sample cards.csv
# spiCardId, externalId, optional PAN (extra columns ignored) 1499713,9295121628797499, 1499714,9295121628797500,5480391234568598 # header / blank lines that don't match the pattern are silently skipped
- Row pattern
^[0-9]{6,20}\s*,\s*[0-9]{6,20}.*— the first two numeric fields (spiCardId, externalId) are required; non-matching rows are silently skipped. - PAN is optional. Supplied (≥16 digits) → masked locally; omitted → import makes an extra Tribe
getCardNumbercall to fetch + mask + hash it. - Duplicate spiCardId → hard error — the whole run aborts before importing anything.
- On launch it counts the rows and prompts
"…importing N cards? [y/n]"— onlyyproceeds. - This list is usually gen's
out.csvreshaped (see the hand-off above):cardId→spiCardId,cardExtId→externalId.
ctx set Tribe credential
DEFAULT / VISA → selects the RSA keys for the calls below.
tribe getCardDetails(spiCardId)
Reads the card from Tribe. Not on Tribe → VendorException (card fails). Returns program, holder, account, currency, status…
tribe getCardNumber — only if no PAN
Fetches + masks + hashes the PAN when the CSV didn't supply one.
resolve program & existing records
Find PartnerProgram by Tribe program id; look up existing card by SpiId / ExternalId.
idempotent already imported? skip
Re-running the same list is safe.
commit write the card graph
Persist the entities (right) and commit this card's own transaction. One bad card rolls back alone.
- maskedPan, panHash
- homeCurrency, isVirtual
- validUntil, designId
- kyc_locked = false
- requires_kyc = null
- new · status COMPLETE
- get-or-create by SpiId
- KycStatus Unverified
- get-or-create
- spiImport + Tribe status
- links every entity
Concurrency — The Tribe Limit
Tribe throttles / flags us above 6 concurrent calls. Both commands cap their worker pool at 6 — and for these commands, pool size == concurrent Tribe calls.
Card #1 runs synchronously before the pool starts (no overlap); the output/notifier thread never calls Tribe. So the ceiling is exactly 6 per command.
Where Everything Ends Up
Gen's out.csv is the bridge — its card ids become import's input. Skip import and the cards exist on Tribe but never appear in cpm.