Internal Docs/Card provisioning — end to end
test-card provisioning · two acli commands · one lifecycle

card
genimport

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.

00

the whole lifecycle

input
template.csv
3 sections · fields per card
stage 1 · acli
bulkCardGen
≤6 workers → createCard
cardIds
artifact
out.csv
cardId, extId, …
reshape
stage 2 · acli
bulkCardImport
≤6 workers · per-card tx
destination
cpm DB
card graph
↑ 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
01

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

02

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

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 idsExternalId your refpanHash lookup
04

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.
worker · createCard
worker · createCard
worker · getCardDetails
worker · getCardDetails
worker · createCard
worker · getCardDetails
7th+ · waits
6
max in flight
Tribe
RSA /api

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.

05

where everything ends up

template
your input
→ gen →
Tribe
card created
→ import →
cpm DB
card graph mirrored

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.