# Encrypt sensitive fields in your payment requests With field-level encryption, you encrypt sensitive values in your request body with a public key that Checkout.com issues to you. You then send the encrypted values to our API. Only Checkout.com holds the matching private key, so the values are not readable in transit or in any system between you and us. This includes your own logs, proxies, and load balancers. You can encrypt individual fields, like the card number, or the entire request body. > **Information** > > To enable field-level encryption, contact your account manager or [request support](https://dashboard.checkout.com/support/new?topic=case_account_management_and_access&issue=issue_account_changes). --- ## How it works 1. You generate an RSA key pair with Checkout.com. You keep the public key and its key ID (`kid`). We store the private key securely on our side. 2. You encrypt the values you want to protect as JWE (JSON Web Encryption) tokens, using the compact serialization format and the public key. 3. You send the request with the encrypted values instead of the plain values. In the `Cko-Encryption-Key-Id` request header, declare the `kid` you used, and in the `Cko-Encrypted-Properties` header, declare the fields you encrypted. 4. We match your `kid` to the private key, decrypt the values, and process the request as if you had not encrypted it. 5. Your response is unchanged. Field-level encryption applies to requests only. You can encrypt the entire request body, or encrypt specific fields within it. | What you encrypt | What it covers | Headers to send | | --- | --- | --- | | The entire request body | Every field in the request. The body you send is the JWE token itself, not JSON. | `Cko-Encryption-Key-Id` only. There is no JSON path to declare, so omit `Cko-Encrypted-Properties`. | | `source` | The entire source object, including the card number, expiry date, name, and billing address. | `Cko-Encryption-Key-Id` and `Cko-Encrypted-Properties: source` | | `source.number` | The card number only. | `Cko-Encryption-Key-Id` and `Cko-Encrypted-Properties: source.number` | | `destination` | The entire destination object in a payout request, including the card number, expiry date, and account holder details. | `Cko-Encryption-Key-Id` and `Cko-Encrypted-Properties: destination` | | `destination.number` | The destination card number in a payout request only. | `Cko-Encryption-Key-Id` and `Cko-Encrypted-Properties: destination.number` | We reject any request that encrypts an unsupported field with a `422` response and the `invalid_encryption_property` error code. You can encrypt at most two fields per request. We reject a request that declares more with the `encrypted_field_limit_exceeded` error code. Encrypting the entire request body counts as one operation, so the limit does not apply to it. --- ## Before you start Before encrypting any fields, make sure you have: - Field-level encryption enabled for your client ID - Your public key in PEM (Privacy-Enhanced Mail) format, and the key ID Checkout.com issued with it - A JWE library that supports RSA-OAEP-256 key encryption and A256GCM content encryption > **Warning** > > Never send us your private key, and never use a key pair you generated yourself. If your key pair is compromised, contact your account manager or [request support](https://dashboard.checkout.com/support/new?topic=case_account_management_and_access&issue=issue_account_changes) to get a replacement. --- ## Supported algorithms Every token you send must carry a protected header with the following values. Checkout.com rejects any other algorithm. | Header | Required value | | --- | --- | | `alg` | RSA-OAEP-256 | | `enc` | A256GCM | | `kid` | The key ID we issued to you. It must match the `Cko-Encryption-Key-Id` header on the request. | --- ## Encrypt and send a request To send an encrypted request, encrypt each value, and then add the encrypted values and the encryption headers to your request. ### Encrypt the field values Convert the value to JSON first, and then encrypt that JSON. A string value keeps its surrounding quotes, so the value you encrypt for a card number is `"4242424242424242"`, not `4242424242424242`. An object value is encrypted as its JSON representation, for example `{"type":"card","number":"4242424242424242","expiry_month":12,"expiry_year":2030}`. Encrypt each field separately. Each field becomes its own JWE token. The following examples use JavaScript, Java, and Python, but any library that supports RSA-OAEP-256 and A256GCM works. In each example, replace the placeholder values with your public key and `kid`. **JavaScript** ```js import { readFileSync } from 'node:fs'; import { CompactEncrypt, importSPKI } from 'jose'; const CKO_PUBLIC_KEY_PEM = readFileSync('cko-public-key.pem', 'utf8'); const CKO_KEY_ID = 'key_01234'; async function encryptField(value) { const publicKey = await importSPKI(CKO_PUBLIC_KEY_PEM, 'RSA-OAEP-256'); return new CompactEncrypt(new TextEncoder().encode(JSON.stringify(value))) .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM', kid: CKO_KEY_ID }) .encrypt(publicKey); } encryptField('4242424242424242').then((encryptedNumber) => { console.log(encryptedNumber); }); ``` **Java** ```java import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.EncryptionMethod; import com.nimbusds.jose.JWEAlgorithm; import com.nimbusds.jose.JWEHeader; import com.nimbusds.jose.JWEObject; import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.RSAEncrypter; import java.security.interfaces.RSAPublicKey; // Load your Checkout.com public key from the PEM file RSAPublicKey ckoPublicKey = loadPublicKey("cko-public-key.pem"); String ckoKeyId = "key_01234"; ObjectMapper objectMapper = new ObjectMapper(); JWEObject jwe = new JWEObject( new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM) .keyID(ckoKeyId) .build(), new Payload(objectMapper.writeValueAsString("4242424242424242"))); jwe.encrypt(new RSAEncrypter(ckoPublicKey)); String encryptedNumber = jwe.serialize(); ``` **Python** ```python import json from jwcrypto import jwk, jwe CKO_KEY_ID = "key_01234" with open("cko-public-key.pem", "rb") as pem_file: key = jwk.JWK.from_pem(pem_file.read()) def encrypt_field(value): token = jwe.JWE( json.dumps(value).encode(), json.dumps({"alg": "RSA-OAEP-256", "enc": "A256GCM", "kid": CKO_KEY_ID}), ) token.add_recipient(key) return token.serialize(compact=True) encrypted_number = encrypt_field("4242424242424242") ``` The result is a compact JWE token, made up of five base64url segments separated by dots. For example: `eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoia2V5XzAxMjM0In0.Xh8...Q.9dK...A.1sP...w.Zt4...g` ### Send the request Call the [Request a payment or payout](https://api-reference.checkout.com/tag/Payments/#operation/requestAPaymentOrPayout) endpoint. > **Information** > > Your base URL's `{prefix}` value is unique to your account and environment. To learn how to retrieve your base URLs for the sandbox and production environments, see [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints.md). **Production** `POST` `https://{prefix}.api.checkout.com/payments` **Sandbox** `POST` `https://{prefix}.api.sandbox.checkout.com/payments` **Encrypt specific fields** Replace each plain value with its JWE token, and add the following headers: - `Cko-Encryption-Key-Id` – The key ID (`kid`) of the key you encrypted with. This header is required. - `Cko-Encrypted-Properties` – A comma-separated list of the JSON paths you encrypted, for example `source.number`. Omit it only when you encrypt the entire request body. To encrypt only the card number, send the following request, with the `Cko-Encrypted-Properties: source.number` header: ```json { "source": { "type": "card", "number": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoia2V5XzAxMjM0In0.Xh8...Q.9dK...A.1sP...w.Zt4...g", "expiry_month": 12, "expiry_year": 2030 }, "amount": 1000, "currency": "GBP" } ``` To encrypt the entire source object, set `Cko-Encrypted-Properties` to `source` and send the following request body: ```json { "source": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoia2V5XzAxMjM0In0.Xh8...Q.9dK...A.1sP...w.Zt4...g", "amount": 1000, "currency": "GBP" } ``` To encrypt the destination in a payout request, follow the same pattern. Set `Cko-Encrypted-Properties` to `destination` to encrypt the entire destination object, or to `destination.number` to encrypt the card number only. **Encrypt the entire request body** To encrypt everything, convert the whole request body to JSON, encrypt it as a single JWE token, and send that token as the request body. There are no fields to declare, so omit the `Cko-Encrypted-Properties` header: ```bash curl https://{prefix}.api.sandbox.checkout.com/payments \ -H 'Authorization: Bearer sk_sbox_wjvrysklsqjmrhn3yoexnshsl72' \ -H 'Content-Type: application/json' \ -H 'Cko-Encryption-Key-Id: key_01234' \ -d 'eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoia2V5XzAxMjM0In0.Xh8...Q.9dK...A.1sP...w.Zt4...g' ``` > **Information** > > Keep `Content-Type: application/json`. The decrypted value must be a JSON object. Checkout.com rejects a JSON array or any other value with the `request_body_malformed` error code. If the request is valid, you receive the same response as for the equivalent request without encryption. --- ## Understand encryption behavior The following rules apply to encrypted requests: - You can adopt encryption one field at a time. If a path you declare in `Cko-Encrypted-Properties` holds a plain value, Checkout.com passes it through untouched. This lets you roll out encryption gradually. We ignore duplicate paths in the header. - Always send the `Cko-Encryption-Key-Id` header. If you send an encrypted body without it, or before your client is enabled, we do not decrypt the body. The JWE token reaches validation as plain text, and the request fails with a standard validation error. - Use one key ID per request. You must encrypt all tokens in a request with the key named in `Cko-Encryption-Key-Id`. - Field-level encryption applies to requests only. We never encrypt responses, webhooks, or events with your key. --- ## Resolve errors Failures return the standard validation error shape, with the specific reason in `error_codes`. ```json { "request_id": "0HN7ROOFDVJHQ:00000001", "error_type": "request_invalid", "error_codes": ["invalid_encryption_key"] } ``` | Status | Error code | Cause | How to fix | | --- | --- | --- | --- | | `422` | `invalid_encryption_key` | The key ID is unknown, belongs to another account, or is not available in the region that serves your request. Alternatively, the `kid` in the JWE header does not match the `Cko-Encryption-Key-Id` header. | Send the key ID that Checkout.com issued to you. Make sure every token declares the same key ID in its protected header. | | `422` | `invalid_encryption_payload` | The token is not a well-formed JWE for the declared algorithms. For example, the encrypted key is unreadable, the IV (initialization vector) or authentication tag has the wrong length, or the encrypted data failed authentication. | Encrypt the value again. Do not change or re-encode the token after your library produces it. | | `422` | `decryption_error` | We could not decrypt the token with the key mapped to your key ID. | Confirm that you encrypted with the current public key for that key ID, and then retry. If the error persists, contact your account manager. | | `422` | `encrypted_field_limit_exceeded` | The request declares more than two fields in `Cko-Encrypted-Properties`. | Declare at most two paths, or encrypt the entire source or destination object instead. | | `422` | `invalid_encryption_property` | A declared path is not a supported field. | Declare only `source`, `source.number`, `destination`, or `destination.number`. | | `400` | `request_body_malformed` | The body is neither JSON nor a valid JWE, the JWE declares an unsupported `alg` or `enc`, or the decrypted body is not a JSON object. | Check that the protected header declares RSA-OAEP-256 and A256GCM, and that the value you encrypted is valid JSON. | --- ## Test your integration To test your integration in the sandbox: 1. Use your sandbox key ID and public key to send a payment with `source.number` encrypted. For card numbers to use, see [Test cards](https://www.checkout.com/docs/developer-resources/testing/test-cards.md). 2. Confirm that you receive a `201` response with `"approved": true` and the expected card details. This confirms that Checkout.com decrypted your token. 3. Change one character of the token and send the request again. You receive a `422` response with the `invalid_encryption_payload` error code. 4. Send the same request with a key ID that Checkout.com did not issue to you. You receive a `422` response with the `invalid_encryption_key` error code. --- ## Move to production and rotate keys After your sandbox tests pass and your client ID is enabled in production, roll out encryption to your production traffic. To rotate keys, request a new key pair, start encrypting with the new key ID, and retire the old key after all your traffic has moved to the new one.