Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

Security Model

Firmly is a commerce backend that handles authentication, payment-credential encryption, and data isolation across merchants. This page covers the security boundaries that matter to destinations and merchants integrating Firmly.

​​ Authentication boundaries

Firmly uses two distinct authentication patterns. Both result in a per-request x-firmly-authorization value — a short-lived JWT for browser sessions, a long-lived secret for server-to-server.

Pattern Where the code runs Credentials
Browser session Client-side (browser, mobile app, ad surface, embed) App ID exchanged for short-lived JWT (~1 hour)
Server-to-server (S2S) Backend (destination server, integration tier) Long-lived secret + device identifier per user

See Authentication for the full request/response shapes and renewal patterns.

Why two patterns: client-side code can’t safely hold a long-lived secret, so it exchanges a relatively safe identifier (App ID) for a short-lived token. Server-side code can hold a long-lived secret, so it skips the bootstrap step.

​​ Card encryption — JWE

Card payment payloads are encrypted client-side before they ever reach Firmly’s servers.

Step What happens
1. Get public key GET /payment/key returns an RSA JWK
2. Encrypt locally Client encrypts the card with RSA-OAEP-256 + A256GCM (JWE)
3. Send encrypted payload POST /payment/place-order with the JWE in the request body
4. Decrypt server-side Firmly’s vault decrypts the card in memory and submits it over TLS to the payment path for that merchant (see below)
5. Discard The plaintext card never persists on Firmly’s servers; the JWE payload is also discarded post-order

This means the destination’s environment doesn’t hold cleartext card numbers at rest. The encryption happens before the network call.

After decryption, the vault routes the card one of two ways, depending on the merchant’s payment setup:

  • PSP tokenization — the vault submits the card to the merchant’s payment processor (PSP) over TLS; the PSP returns a token, and Firmly uses that token to place the order on the merchant’s platform.
  • Direct to the merchant’s platform — for merchants that process payments themselves (no separate PSP), the vault submits the card to the merchant’s own platform over TLS.

In both cases the card is decrypted only in memory, and the cleartext card is never stored.

​​ PCI scope boundary

PCI DSS scope is determined by where cleartext card data lives. With Firmly’s JWE flow:

Location Holds cleartext PAN? In PCI scope?
Destination’s client (browser, app) Briefly, during the JWE encryption step Yes — destination’s responsibility
Destination’s server No (JWE payload only) Reduced scope
Firmly’s API edge No (JWE only) Yes — Firmly’s responsibility
Firmly’s vault Briefly, in memory during decryption Yes — Firmly’s responsibility
Merchant’s PSP Yes, for processing PSP’s responsibility

For destinations using JWE encryption, this reduces PCI scope toward SAQ A-EP — the self-assessment questionnaire for merchants whose payment pages are partially delivered by their own systems but who don’t store cardholder data — for the client-side encryption surface. Confirm your exact PCI assessment with your compliance team.

​​ Audits and certifications

Firmly maintains current third-party audits for both data security and payment-card handling:

Certification Scope
PCI DSS v4.0.1 Card data handling — Firmly’s vault and the JWE encryption flow are in-scope
SOC 2 Type II Security, availability, and confidentiality controls across Firmly’s commerce platform

Certification artifacts (current reports, attestation letters, scope statements) are available under NDA during commercial onboarding.

​​ Per-merchant data isolation

A destination’s App ID is scoped to a specific set of merchants — in sandbox to a paired test merchant, in production to the merchants the destination is approved to transact against.

Boundary How it’s enforced
Discovery results POST /discovery/search returns products only from merchants the App ID can reach
Cart access POST /cart/... calls with a {domain} outside the App ID’s scope return DomainNotFound
Order metadata Orders carry the originating destination’s identifier; merchants only see orders from destinations they’ve opted into

This means one destination cannot accidentally transact against a merchant it’s not approved for, even if it knows the merchant’s domain.

​​ Session lifetime and renewal

Browser-session tokens expire after ~1 hour (expires_in: 3600). After expiry:

  • The token returns 401 on subsequent calls
  • The destination calls POST /browser-session again with the expired token in the body to renew without losing the device_id
  • Renewal isn’t a separate server-enforced window — it works while the session state is still retained (~7 days from last activity in Firmly’s session store); once that state is cleared, a fresh session bootstrap is required

This is shorter than typical e-commerce session lifetimes, which constrains the blast radius if a token is exposed.

​​ What Firmly never sees

Specifically excluded from Firmly’s systems:

  • Cleartext PAN at rest — JWE encryption removes this from storage
  • Destination’s internal business logic — Firmly receives the user-facing intent + payment + address; not the destination’s pricing models, internal user state, etc.