Skip to content

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.

  1. Your backend requests a session encryption key.
  2. Your backend passes the key material and key_id to your frontend.
  3. The frontend encrypts the card fields and returns the encrypted payload, along with the field layout, card brand, and last4.
  4. Your backend creates the payment with that payment_method object in place of raw card fields.

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 here

Pass the public key material and key_id to your frontend — neither is secret. Keep your sk_ key on the server.

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 response
import * 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.

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.

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.

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.