Skip to main content
Get secure card details returns card credentials encrypted. The card number and CVV are sealed with a public key you register with us, so no readable PAN passes through the gateways, proxies, log stores, or error trackers that handle the response in transit. Decryption takes place in your own systems, using the private key that pairs with the registered public key. We never receive that private key and cannot decrypt the response ourselves. Setup is two steps: generate an RSA key pair, then register the public key. Once encrypted details are on for your company, you decrypt each card details response in your own code.

Key pair roles

Setup produces one RSA key pair: two files with distinct roles. Mixing them is the most common cause of integration failure. Only the public key leaves your environment. It encrypts each response; the corresponding private key is the only key that can decrypt one. Decryption always uses the private key.
Disclosure of the private key voids the protection entirely: any holder can decrypt every response encrypted under the matching public key. We do not retain a copy and cannot restore it. If the key is lost or compromised, generate a new pair and register the new public key.

Step 1: Generate your keys

Generate an RSA key pair of 2048 bits or greater. The five implementations below are equivalent; select a language from the dropdown. On Windows use JavaScript, Python, Go or PowerShell; the Shell option needs openssl, which Windows does not install by default.

Store the private key

Move bitnob-cards-private.pem into your secret manager and delete the local copy. Pipe the file directly rather than printing and pasting it. The key is not recoverable if lost, and we hold no copy.
Store the private key
At decryption time, read the key from the secret store rather than from the repository or a build image. The decryption examples load the key from the path in BITNOB_CARDS_PRIVATE_KEY_PATH.

Verify the key format

Confirm the first line of the public key before proceeding. An incorrect header is the most common cause of rejection in step 2:
Confirm the public key format
The output must be -----BEGIN PUBLIC KEY-----. Any other header, such as -----BEGIN RSA PUBLIC KEY-----, is rejected in step 2; see when something goes wrong for the conversion.

Step 2: Register the public key

Register the public key from the dashboard, under Settings → Controls → Card encryption key, or with Register an encryption key. Open Settings in the dashboard. Select the Controls tab. Select Card encryption key. Click on Add new key, paste the contents of bitnob-cards-public.pem into the field, and save.
To skip the navigation, open Settings → Controls → Card encryption key directly.

What to paste

The whole file, both armor lines included, with the line breaks exactly as they appear in the file:
bitnob-cards-public.pem
The armor lines are how the key is located at all; a value without them is rejected regardless of how well formed the rest is. Line breaks inside the base64 body are optional: wrapped at 64 characters (what the step 1 generators produce) and a single unbroken run are both accepted.
Copy the file; do not retype or hand-edit it. Every rejection listed in when something goes wrong comes from a key that was reassembled by hand or truncated in transit. Do not paste a JSON-escaped value either: literal \n sequences are not valid base64 and will be rejected.

Confirm the key is complete

A 2048-bit public key from step 1 always has the same shape, so a truncated paste is visible without running anything: Nine lines: the two armor lines and seven lines of base64. The body ends IDAQAB. This encodes the public exponent 65537, which every key generated in step 1 uses. If the value is shorter, or does not end that way, it was clipped between the file and the dashboard. Copy the file again. On macOS, Linux, or Git Bash you can count the base64 body directly. It is 392 characters:
Check the base64 body
Replace /path/to/ with the folder you generated the key in. The ----- is not a placeholder. It is the text grep matches to strip the BEGIN and END lines, leaving only the base64 body to count.
Registering a key again replaces the stored one, which is how you rotate. See rotating your key.

Decrypting a response

Decryption uses the private key, the half that never left your machine. The example below fetches a card and returns the plain details; select a language from the dropdown. It depends only on the generateAuthHeaders helper from Authentication.
This completes the integration. Validate it against the test vector before calling the endpoint with a live card.

Test vector

A decryption failure against a live card has two possible causes, a defect in your decryption code or an incorrect key registration, and both present identically. The vector below isolates them. It is a genuine encrypted payload built with the scheme documented on this page, and requires no API call: if your code decrypts it successfully, your implementation is correct and the fault lies in registration.
Test key only. The private key below is published intentionally and is for testing only. It secures nothing. Do not use it in your integration, and do not confuse it with bitnob-cards-private.pem, which you generated in step 1 and must never disclose.
test-private-key.pem
Feed your decrypt function this encrypted_details object, using the key above:
encrypted_details
Passing both to your decrypt function must return this object:
Expected plaintext
If the vector decrypts but a live card does not, the implementation is correct and the registration is at fault; see check your key pair. If the vector fails, the defect is in your decryption code; see when something goes wrong.

What you get back

When encrypted details are on, the response carries data.encrypted_details:
Response
data.balance_amount is a STRING. The API serializes 64-bit integers as JSON strings, so it arrives as "5000000", not 5000000. Convert it (Number(...) / int(...)) before doing arithmetic. The copy inside the encrypted blob is a real number; see below.

What decryption gives you

Decrypting encrypted_details yields the card itself, with the balance carried alongside it as a real number:
Decrypted details
The balance is therefore available two ways: from data.balance_amount without decrypting anything (convert the string), or from the decrypted object (already a number). data.balance_amount and data.balance_currency sit outside the encrypted blob deliberately, so you can show a balance without decrypting anything.

When something goes wrong

Decryption fails

A GCM authentication failure is never safe to work around by skipping the tag check. It means the bytes are not the ones we sealed. If you have not already done so, run the test vector to establish whether the fault lies in your decryption code or in your key registration.

Registration is rejected

Fetching card details fails

Invalid PEM format

Every value below is rejected with this message or invalid encryption key. All of them come from hand-editing the key instead of copying the file verbatim:

Check your key pair

If the test vector passes but a live card fails, the private key you are decrypting with is probably not the mate of the public key we hold. The two hashes must match:
Check the key pair
If they differ, the two files are from different pairs. Regenerate (step 1) and register the new public key (step 2). If the hashes match, the key registered with us may differ from the file you hold. Re-register bitnob-cards-public.pem to confirm the stored key corresponds to your private key.

Handling the decrypted details

The decrypted payload contains a live card number and CVV. PCI scope applies to it and to any system it reaches. Never log it, at any level. Log the card_id instead. Do not persist it. Retrieve the details again when required; we do not store them either. Keep it out of URLs, query strings, analytics and error trackers. Return it to a browser only over TLS, and only to the cardholder.

Rotating your key

Generate a new pair (step 1) and register the new public key (step 2); it replaces the old one. Rotation is not retroactive: a response already encrypted to the old key can only be opened with the old private key. Keep the previous private key until you are certain nothing you fetched under it is still in flight.

How it works

Envelope encryption, the same shape as JWE. RSA cannot encrypt a payload this size (a 2048-bit OAEP-SHA256 key holds at most 190 bytes, and the details JSON is larger), so: Both fields are standard, padded base64. The AES key is generated per request and never stored on our side. We hold only your public key, so we cannot decrypt what we send you, by design rather than by policy.