How to Create a Card¶
This guide covers the per-cardholder half of card issuance: turning an existing card product into a card, and following it to ACTIVE. The one-time setup it depends on — product, layout, certified embosser — is Manage Card Products.
Time to complete: ~10 minutes
API: Issuer API — base path /api/issuer
Prerequisites¶
- An access token for an
issuertenant with thecreate-cardormanage-cardrole. See Authentication. - An issuable product. It must be active and have a layout with an approved embosser. Confirm with
GET /api/issuer/card-products/issuing-data— a product missing from that list cannot issue a card. See Manage Card Products. - The ledger accounts the card will be backed by. Which endpoint you call depends on whose ledger holds them — see the next section.
export TOKEN="eyJhbGci..."
export BASE="https://sandbox.qpay.quecto.com.br"
export ISSUER="$BASE/api/issuer"
Which endpoint issues your card¶
A card is worthless without accounts behind it, so who holds those accounts decides where you start.
Do not use this guide's Step 3. Call POST /api/issuer/ledger/accounts/cards instead. The ledger creates the card's accounts, then calls the Issuer API for you with the account links already filled in. Steps 1, 2 and 4 here still apply, and the card you get back is the same card.
Use this guide. You create the accounts in your own ledger first and pass their IDs as account_links in Step 3. See Connect Your Own Ledger for registering the callback endpoints QPay will authorize against.
Overview¶
flowchart TD
A[Import a certificate - once] --> B[Pick the product and layout]
B --> C[Create the card]
C --> D[Monitor status]
C -. optional .-> P[Supply your own PIN]
Step 1 — Import a certificate (once per issuer)¶
QPay uses your issuer certificate to establish trust for the cryptographic operations that happen during authorization — ARQC validation and ARPC generation — and it is what a caller-supplied PIN block is keyed against. Import one before issuing cards.
Generate an ECC P-256 certificate signing request with your HSM or openssl:
openssl ecparam -name prime256v1 -genkey -noout -out issuer.key
openssl req -new -key issuer.key -out issuer.csr \
-subj "/CN=My Issuer/O=My Organization/C=BR"
# Base64-encode the CSR (single line, no line breaks)
CSR_B64=$(base64 -w 0 issuer.csr)
Submit it:
curl -s -X POST $ISSUER/certificates \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"certificate_request_pem\": \"$CSR_B64\"}" | tee cert.json
Example response:
{
"certificate_id": 42,
"hsm_key_reference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"certificate_pem_base64": "LS0tLS1CRUdJTi..."
}
GET /api/issuer/certificates lists your certificates with an expires_at for each — rotate before that date, or authorization begins failing. This step is only needed once, and again at rotation.
You need certificate_id, not hsm_key_reference
certificate_id — the integer 42 above — is what a PIN block references later. QPay resolves it to the HSM key server-side; passing the key reference, an alias or an ARN there is rejected.
Step 2 — Pick the product and layout¶
[
{
"product_id": 101,
"name": "Classic Debit",
"description": "Standard UnionPay debit card",
"currency_numeric_code": 986,
"layouts": [ { "layout_id": 15, "prints_pan_on_face": false } ],
"credit_application_aids": []
}
]
This listing is already filtered to what can be issued today, so anything it returns will pass the checks in Step 3. If your product is absent, go back to Manage Card Products — it is inactive, has no layout, or has no approved embosser on one.
Step 3 — Create the card¶
curl -s -X POST $ISSUER/cards/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"product_id\": $PRODUCT_ID,
\"layout_id\": $LAYOUT_ID,
\"reference_id\": \"MY-UNIQUE-REF-001\",
\"card_holder_name\": \"JOAO DA SILVA\",
\"billing_address\": {
\"name\": \"Joao da Silva\",
\"postal_code\": \"01310-100\",
\"country\": \"BR\",
\"state\": \"SP\",
\"city\": \"São Paulo\",
\"address_line1\": \"Av. Paulista 1000\",
\"address_line2\": \"Apto 42\"
},
\"shipping_address\": {
\"name\": \"Joao da Silva\",
\"postal_code\": \"01310-100\",
\"country\": \"BR\",
\"state\": \"SP\",
\"city\": \"São Paulo\",
\"address_line1\": \"Av. Paulista 1000\",
\"address_line2\": \"Apto 42\"
},
\"account_links\": {
\"A0000003330101\": {
\"account_id\": \"0192f3b2-1111-7000-9c4e-1f2a3b4c5d61\",
\"float_account_id\": \"0192f3b2-2222-7000-9c4e-1f2a3b4c5d62\"
}
}
}" | jq .
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
product_id |
integer | Yes | The product the card is issued under; must be active |
layout_id |
integer | Yes | A layout of that product; a layout from another product is rejected |
reference_id |
string | Yes | Your identifier for this card. Not to be confused with the embosser's embosser_reference_id on the product |
card_holder_name |
string | Yes | Name to emboss, uppercase |
billing_address |
object | Yes | name, postal_code, country, state, city, address_line1; address_line2 and complement optional |
shipping_address |
object | Yes | Same shape — where the plastic is delivered |
account_links |
object | Yes | AID → { account_id, float_account_id }, one entry for every application on the template that has a BIN. Both are UUIDs from the ledger holding the accounts |
product_embosser_id |
integer | No | Produce this card at a specific ProductEmbosser. Omit to use the layout's highest-priority approved one |
pin |
object | No | Caller-supplied PIN material. Omit and the HSM generates a random PIN — see below |
Example response:
{
"reference_number": "MY-UNIQUE-REF-001",
"card_id": 5821,
"embosser_id": 3,
"status": "PRODUCTION_STATUS_APPROVED",
"embossing_status": "EMBOSSING_PENDING"
}
reference_number echoes the reference_id you sent, so you can reconcile the response against your own record. errors appears only when there are any.
status here is the production status, not the card's status
PRODUCTION_STATUS_APPROVED means the production request was accepted (the alternatives are PRODUCTION_STATUS_SCHEDULED, PRODUCTION_STATUS_REJECTED and PRODUCTION_STATUS_PARCIAL in a batch). The card itself is created PROVISIONED and enters the embossing queue — that is the status Step 4 follows.
The AIDs come from the card template, not from you
A missing or zero-UUID entry in account_links fails the request with missing account link for application "<AID>". The set of AIDs that need one is whatever the product's card template declares with a BIN — read them from the template listing in Manage Card Products.
Step 4 — Monitor card status¶
GET /api/issuer/cards lists every card with its product, layout, BIN and per-application status.
Card status lifecycle:
| Status | Meaning |
|---|---|
PROVISIONED |
Card created, awaiting embossing and delivery |
ACTIVE |
Embossed and delivered — card is operational |
FROZEN |
Temporarily suspended; reversible |
BLOCKED |
Blocked — see the card's block records |
CANCELLED |
Permanently withdrawn; terminal |
A card leaves PROVISIONED when the embosser reports progress through the Embosser API — nothing advances it on its own, in the sandbox or anywhere else. If your cards sit at PROVISIONED, the bureau has not reported yet.
Advanced — supply your own PIN¶
Omitting pin is the normal path: the HSM generates a random PIN and the cardholder gets it through your usual channel. Supply one only when the cardholder chose it in your own environment — QPay never accepts a plaintext PIN.
Your HSM must, inside your secure environment:
-
Fetch the QPay HSM public key certificate and use it to agree a key:
-
Build an ISO 9564 PIN block for the cardholder's PIN, and encipher it under the PIN encryption key derived from that agreement.
- Sign your ephemeral public key with the private key of the certificate from Step 1.
Then add a pin object to the Step 3 body:
"pin": {
"enciphered_pin_block": "A1B2C3D4E5F60708",
"incoming_pek": {
"key_reference": "my-hsm-key-alias",
"wrapped_key": {
"wrapped_key_type": "ECC",
"ecc_wrapped_key": {
"ca_public_key_identifier": "42",
"signed_public_key": "LS0tLS1CRUdJTi...",
"derived_key_algorithm": "3DES_3KEY",
"derivation_function": "NIST_SP800_108",
"derivation_hash_algorithm": "SHA_256",
"shared_information": "3f1a...64 hex characters..."
}
}
}
}
| Field | Description |
|---|---|
ca_public_key_identifier |
The certificate_id from Step 1, as a string — QPay resolves it to the HSM key itself |
signed_public_key |
Your ephemeral public key, PEM, base64-encoded, signed by that certificate's CA private key |
derived_key_algorithm |
3DES_2KEY or 3DES_3KEY |
derivation_function |
NIST_SP800_108 or ANSI_X9_63 |
derivation_hash_algorithm |
SHA_256 or SHA_512 |
shared_information |
Exactly 64 hex characters — 32 random bytes fed into the derivation |
A TR-31 key block is accepted instead: "wrapped_key_type": "TR31" with "tr31_wrapped_key": { "key_block": "..." }.
Issuing multiple cards (batch)¶
For bulk issuance use the batch endpoint rather than looping over /cards/create:
The body takes a request_id (your idempotency key) and card_batch_info, an array whose entries carry the same per-card fields as above — reference_id, cardholder_name, layout_id, both addresses, account_links, and optionally pin. The response returns a batch_id and a per-card status, which you poll:
A batch is accepted whole (ACCEPTED), in part (PARTIAL), or not at all (REJECT) — always read the per-card statuses, since a partial batch still produced cards.
Related¶
- Manage Card Products — the product, layout and embosser this guide assumes
- Use the QPay Ledger — issuing a card whose accounts QPay holds
- Connect Your Own Ledger — issuing against your own accounts
- Issuer API reference — interactive endpoint documentation
- Credentials & authentication — token management