Encrypting the tax id
List Accounts is the one endpoint that accepts the member's tax id in place of a customer_uuid Casap issued. The tax id never travels in the clear: you encrypt it with the public key Casap issued your organization, and Casap decrypts it with the private half, which never leaves Casap.
Every other endpoint takes only the customer_uuid that List Accounts returns.
The key
Casap generates the key pair and hands you the public half at onboarding. It arrives as a PEM block:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
| Property | Value |
|---|---|
| Algorithm | RSA, 2048-bit modulus |
| Format | PEM, BEGIN PUBLIC KEY (X.509 SubjectPublicKeyInfo) |
| Rotation | Not rotated. One key for the life of the integration. |
The key only encrypts, so there is nothing to protect about it: store it in config, check it into your deployment, or load it from a file. It is not a signing key, and the public key Casap publishes for verifying signed messages cannot be used here.
Sandbox and production have separate key pairs. A value encrypted with the sandbox key does not decrypt in production.
Encrypting
| Step | Requirement |
|---|---|
| Encrypt | RSA-OAEP with SHA-256 as both the OAEP and MGF1 digest, and an empty label |
| Encode | Base64 the ciphertext |
| Send | Percent-encode the base64 before putting it in the query string |
The plaintext is the tax id. Only its digits are read, so 123-45-6789 and 123456789 produce the same member.
With openssl:
printf '123456789' \
| openssl pkeyutl -encrypt -pubin -inkey casap-public-key.pem \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha256 \
-pkeyopt rsa_mgf1_md:sha256 \
| base64 -w0
In Node, oaepHash sets both digests at once:
import { constants, publicEncrypt } from 'node:crypto'
const ciphertext = publicEncrypt(
{ key: casapPublicKeyPem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
Buffer.from('123456789'),
)
const customerUuid = encodeURIComponent(ciphertext.toString('base64'))
In Python, both digests are named separately:
from base64 import b64encode
from urllib.parse import quote
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
public_key = serialization.load_pem_public_key(casap_public_key_pem)
ciphertext = public_key.encrypt(
b"123456789",
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
)
customer_uuid = quote(b64encode(ciphertext).decode())
Then send it as customerUuid:
GET /v1/accounts?customerUuid=ZXhhbXBsZSBjaXBoZXJ0ZXh0...
Checking your output
A 2048-bit key produces 256 bytes of ciphertext, which is 344 base64 characters ending in ==. A value of any other length means the padding or the key is wrong, and the call will be refused.
URL-safe base64 is accepted too, with or without padding, so tooling that emits - and _ instead of + and / is fine.
The same tax id encrypts to a different value every time. OAEP adds random padding, so two calls for the same member never match. Do not use the value as a cache key or compare it against a stored one.
Common failures
| Response | Cause |
|---|---|
400 customer_uuid is neither an identifier we issued nor base64 |
The value is not base64. A percent-encoded value that was never decoded does this. |
400 customer_uuid is not the expected size for the organization's key |
The ciphertext is not one block of the key. Usually a truncated or double-encoded value. |
400 customer_uuid is not a recognized customer identifier |
The ciphertext did not decrypt. Almost always MGF1 left on SHA-1, which many OAEP libraries default to. |
400 the organization has no encryption key provisioned |
No key pair has been issued for your org yet. Contact Casap. |
If you hit the decrypt failure, check the MGF1 digest first.
Raw base64 contains +, / and =, which break query strings unless percent-encoded. That shows up as either of the first two errors above.