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-sessionagain with the expired token in the body to renew without losing thedevice_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.
Related
- Authentication — request/response shapes for browser-session and S2S
- Lifecycle and States — session, cart, and order states
- Data Flow — what data moves where
- Errors & Conventions — auth and session errors