Encrypting client-side
Paygasus Pay supports client-side encryption of card data. Your backend requests a short-lived session encryption key, your frontend encrypts the card fields in the browser, and only the encrypted payload transits your servers. Cleartext card data never touches your systems, which significantly reduces your PCI DSS scope.
The encrypted payload is opaque to Paygasus as well — we forward it to the card processor, which holds the corresponding private key. Neither your systems nor ours can decrypt it.
How it works
Section titled “How it works”- Your backend requests a session encryption key.
- Your backend passes the key material and
key_idto your frontend. - The frontend encrypts the card fields and returns the encrypted
payload, along with the fieldlayout, cardbrand, andlast4. - Your backend creates the payment with that
payment_methodobject in place of raw card fields.
1. Request a session key
Section titled “1. Request a session key”Session keys are created server-side with your secret key. Never call this endpoint from the browser.
const response = await fetch( "https://api.paygasus.com/payments/card/services/keys", { method: "POST", headers: { Authorization: "Bearer sk_test_..." }, },);const key = await response.json();TODO: paste a real test-mode response herePass the public key material and key_id to your frontend — neither is
secret. Keep your sk_ key on the server.
2. Encrypt in the browser
Section titled “2. Encrypt in the browser”Encrypt the card fields into a JWE using the session key, and record the
layout — the field order and cleartext lengths that were encrypted. The
layout must exactly describe the contents of the encrypted payload.
// TODO: confirm alg/enc against the keys responseimport * as jose from "jose";
async function encryptCard(sessionKey, card) { const fields = [ { field: "card_number", value: card.cardNumber }, { field: "expiration_month", value: card.expMonth }, { field: "expiration_year", value: card.expYear }, { field: "security_code", value: card.cvv }, ];
const publicKey = await jose.importSPKI(sessionKey.public_key, "RSA-OAEP-256");
const plaintext = fields.map((f) => f.value).join(""); const payload = await new jose.CompactEncrypt( new TextEncoder().encode(plaintext), ) .setProtectedHeader({ alg: "RSA-OAEP-256", enc: "A256GCM", kid: sessionKey.key_id }) .encrypt(publicKey);
return { key_id: sessionKey.key_id, payload, layout: fields.map(({ field, value }) => ({ field, len: value.length })), brand: detectBrand(card.cardNumber), // e.g. "VISA", "AMEX" last4: card.cardNumber.slice(-4), };}Send the returned object to your backend along with whatever non-sensitive
checkout data you need. Do not send the raw card fields. brand and
last4 are not sensitive and are sent in the clear for display and
routing.
3. Create the payment
Section titled “3. Create the payment”On your backend, create the payment with the encrypted payment_method in
place of raw card fields:
const response = await fetch("https://api.paygasus.com/payments/card", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ amount: 1000, currency: "USD", capture_method: "automatic", description: "Order #1042", payment_method: { brand: "AMEX", key_id: "cek_01J9ZK7Q3W8XN4V2M6T0RD5PBA", last4: "0005", layout: [ { field: "card_number", len: 15 }, { field: "expiration_month", len: 2 }, { field: "expiration_year", len: 4 }, { field: "security_code", len: 4 }, ], payload: "eyJhbGciOiJSU0Et...", }, }),});Paygasus correlates the key_id with the processor session it was issued
under and forwards the encrypted payload for decryption. The response is a
standard payment object.
Errors
Section titled “Errors”| Code | Meaning |
|---|---|
encryption_key_expired |
The session key has expired. Request a new key and re-encrypt. |
encryption_key_used |
The key_id was already consumed by a previous payment attempt. |
payload_invalid |
The payload could not be decrypted, or the layout does not match its contents. Check field order, declared lengths, and that you encrypted with the matching session key. |
PCI scope
Section titled “PCI scope”With client-side encryption, card data is encrypted before it leaves the cardholder’s browser and your servers only ever handle ciphertext. Consult your QSA, but this typically supports an SAQ A-EP assessment rather than SAQ D. It does not remove your obligation to serve the payment page over TLS or to protect the integrity of the JavaScript performing the encryption.
