Card Provisioning

liveEnd-to-end card provisioning: bulkCardGen creates cards on Tribe, bulkCardImport mirrors them into the cpm database.· 2026-05-28
0

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.

↑ both stages call Tribe (RSA, ≤6 concurrent) — gen writes cards, import reads them back ↑
bulkCardGen (create on Tribe)bulkCardImport (mirror to cpm)Tribe (external)cpm database
1

Stage 1: bulkCardGen — Create on Tribe

docker exec … acli.sh bulkCardGen -input pc-gen.csv -output pc-out.csv -threads 1 -tribeCredential DEFAULT -rundry false

The Input File — A 3-Section Template

[bulk_card_create]

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.

card_program_id · card_design_id · card_currency_ison · card_country_ison · card_virtual · holder_first_name · holder_last_name · holder_address · delivery_address · kyc_completion_level · card_count · …
[create_card_defaults]

request_param,default_value pairs. Any column a row leaves blank falls back here. Unknown param names are rejected.

[address_templates]

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

row value→ blank? → defaults→ blank & address field? → named address template→ else → null · omitted
card_count fans out: the first row above mints 500 identical cards, the second 250 — so the file's batch size is the sum of every row's count. Country/currency codes are normalised to numeric-3. A field that stays null is simply left out of the Tribe request.

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).

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.

bulkCardGen output (out.csv)
cardId,cardExtId,programId,designId,SeqNum,…
1499713,9295121628797499,380107,55,1,…
bulkCardImport input
spiCardId,externalId,[PAN]
1499713,9295121628797499,
cardIdspiCardId  ·  cardExtIdexternalId  ·  PAN optional (else import fetches it from Tribe)
3

Stage 2: bulkCardImport — Mirror into cpm

docker exec … acli.sh bulkCardImport -input cards.csv -threads 6 -progressEvery 10 -tribeCredential DEFAULT

The Input File — A Flat Card List

1499713
spiCardId · Tribe card id
9295121628797499
externalId · your ref
5480391234568598
optional PAN · ≥16 digits
EUR, Gift Italy, …
extra cols · ignored

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 getCardNumber call 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]" — only y proceeds.
  • This list is usually gen's out.csv reshaped (see the hand-off above): cardIdspiCardId, cardExtIdexternalId.
flow · per card (its own committed transaction)

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.

what lands in cpm
PaymentCardEntity
PAYMENT_CARD
  • maskedPan, panHash
  • homeCurrency, isVirtual
  • validUntil, designId
  • kyc_locked = false
  • requires_kyc = null
CardOrderEntity
CARD_ORDER
  • new · status COMPLETE
CustomerAccount
CUSTOMER_ACCOUNT
  • get-or-create by SpiId
AccountOwner
ACCOUNT_OWNER
  • KycStatus Unverified
  • get-or-create
status
ACTION_REQUEST
  • spiImport + Tribe status
EntityExtRef ×N
external refs
  • links every entity
SpiId Tribe ids ExternalId your ref panHash lookup
4

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.

5

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.