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. |
The decrypt failure stays deliberately vague: it does not say whether the padding, the key or the payload was at fault, so the endpoint cannot be used to test candidate values. If you hit it, 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.