Cybersource JWT Authentication with Message-Level Encryption (MLE)
Cybersource REST API authentication has several moving parts: a per-request JWT, a digest of the request body, one or two .p12 keystores, and message-level encryption that wraps the payload in a JWE. The pieces depend on one another, so issues like these can be difficult to troubleshoot.
This guide explains each piece, and the tabs above let you inspect your own artifacts as you go. Everything here is also implemented in a small open-source library, if you prefer to read working code:
- npm package: @ryankleindev/cybs-jwt-client
- Source: github.com/ryankleindev/cybs-jwt-client
The big picture
Every call to the Cybersource REST API carries a freshly signed JWT in the Authorization: Bearer header. The JWT identifies you, because it is signed with your private key, and it carries a digest of the request body so the server can confirm the body was not altered. Message-level encryption then encrypts the body so it is unreadable in transit, and the response comes back encrypted to you.
A request has three parts:
- The JWT: your identity and a digest of the body.
- The digest: that value, computed a specific way.
- MLE: the body encrypted to Cybersource, and the response encrypted back to you.
The JWT
The JWT is signed with RS256 (RSA with SHA-256) using your private key. Two fields in the header matter:
algisRS256.kidis theserialNumberattribute from your signing certificate’s subject. This is not the X.509 serial number, and not your merchant ID. The p12 Explorer tab shows which value this is.
The payload is where Cybersource is specific. A POST request looks like this:
{
"iat": 1718380800,
"exp": 1718380920,
"request-host": "apitest.cybersource.com",
"request-resource-path": "/tms/v2/tokenize",
"request-method": "post",
"iss": "your_merchant_id",
"jti": "f7c1bec0-189c-472a-afe8-c55b1a019c56",
"v-c-jwt-version": "2",
"v-c-merchant-id": "your_merchant_id",
"digest": "ilXZXw+yriDTJxzXbPdvFvEhsEhNFiXT2lIQ6d9GlYU=",
"digest-algorithm": "SHA-256"
}
Notes on the claims:
expis two minutes afteriat, so a token is valid for two minutes. Clock skew beyond that can cause authentication to fail.request-methodis lowercase, andrequest-resource-pathis the path only.digestanddigest-algorithmappear only on body methods (POST, PUT, PATCH); GET and DELETE omit them.issandv-c-merchant-idare your merchant ID, unless you use a meta key (covered below).
Paste a token into the JWT / JWE Decoder tab to see the header and claims, and to check that each required claim is present.
The digest
The digest claim is a SHA-256 hash of the request body, Base64-encoded with standard Base64. Two details account for most digest mismatches:
- It covers the exact bytes you send. If you re-serialize the JSON differently from what goes on the wire (key order, whitespace, a trailing newline), the digest will not match.
- With MLE, you hash the encrypted envelope, not the plaintext. You encrypt the body first, then compute the digest over the resulting
{"encryptedRequest":"..."}JSON, because that is what is transmitted. This is the encrypt-then-digest order.
The Digest Calculator tab computes the SHA-256 for any body, so you can compare it against the digest claim.
The keys in your p12 files
Cybersource provides key material as PKCS#12 (.p12) keystores. There are up to three distinct keys across them:
- Your signing key and certificate. The private key signs the JWT, and the certificate’s subject
serialNumberis yourkid. The common name is your merchant ID, or your portfolio ID for a meta key. - Cybersource’s MLE certificate, with common name
CyberSource_SJC_US. This is the public certificate you encrypt request bodies to. It usually ships inside the request p12. - Your response-decryption key, used to decrypt the encrypted response, often in a separate response p12.
You will also see a CA (issuer) certificate in the bundle. That is the authority, not something you sign or encrypt with. The p12 Explorer tab opens a keystore and labels each certificate by common name, serialNumber / kid, validity, and role.
Message-level encryption (MLE)
MLE wraps the body in a JWE (JSON Web Encryption), adding encryption on top of TLS. Cybersource uses:
algisRSA-OAEP-256, which wraps the random content-encryption key to Cybersource’s RSA public key.encisA256GCM, which encrypts the payload with AES-256-GCM.kidis theserialNumberof the Cybersource certificate you encrypted to.
MLE applies in both directions:
- Request: you encrypt the body to Cybersource’s public certificate. The wire body becomes
{"encryptedRequest":"<jwe>"}, and the digest is taken over that. - Response: you add a
v-c-response-mle-kidclaim naming which of your keys Cybersource should encrypt the response to, then you decrypt it with your response private key.
The Live Test tab runs a sandbox POST /tms/v2/tokenize and shows the full trace: the signed JWT, the request JWE header, the digest, the request headers, and the raw and decrypted response.
Standard key vs. meta key
The two key types differ only in identity:
- Standard key:
issand the signing certificate’s common name are your merchant ID. - Meta key: a portfolio-level key that can act for many merchants.
issand the common name are the portfolio ID, and you still pass the specificv-c-merchant-idon each request.
The claim set, the digest, and MLE are otherwise identical.
The library
The @ryankleindev/cybs-jwt-client library implements all of this and returns a full trace of every artifact: the plaintext body, the signed JWT, the JWE headers, the digest, and the raw and decrypted response.
npm install @ryankleindev/cybs-jwt-client
The full source and usage docs are on GitHub.
Try it
The tabs above are working tools:
- p12 Explorer: see what is in your keystore and which value is your
kid. - JWT / JWE Decoder: break down a token and check every required claim.
- Digest Calculator: confirm your
digestmatches the bytes you are sending. - Live Test: run an end-to-end sandbox tokenize and inspect the trace.
Everything runs against the Cybersource sandbox, and nothing you enter is stored or logged.
.p12 and password are sent to the
function, used in memory, and never stored or logged. Do not upload production keys.
Open a PKCS#12 (.p12) keystore and list its certificates:
common name, the serialNumber that becomes the JWT kid,
validity dates, and which cert is yours vs. Cybersource’s MLE cert.
Paste a compact JWT (3 segments) or JWE (5 segments). For a JWT, every Cybersource-required claim is checked. The signature is not verified; this is a decoder and validator.
Compute the SHA-256 base64 digest over the exact request body. This must
equal the JWT digest claim. With request MLE on, hash the
{"encryptedRequest":…} envelope (the bytes on the wire), not the plaintext.
apitest.cybersource.com) only. Your p12s and passwords are used in memory and
never stored or logged. Do not use production keys or live card data.
Run a real sandbox tokenize call (POST /tms/v2/tokenize,
request & response MLE on) using your sandbox keys, and see the full trace: the
signed JWT, the request JWE header, the digest, request headers, and the raw vs.
decrypted response. Powered by the open-source client library.