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 needsopenssl, which Windows does not install by default.
Store the private key
Movebitnob-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
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
-----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 ofbitnob-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
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 endsIDAQAB. 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
/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 thegenerateAuthHeaders helper from Authentication.
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
encrypted_details object, using the key above:
encrypted_details
Expected plaintext
What you get back
When encrypted details are on, the response carriesdata.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
Decryptingencrypted_details yields the card itself, with the balance carried alongside it as a real number:
Decrypted details
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
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 thecard_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.