Integration security
Credentials, isolation, signatures and transport — with the honest state of each control.
Context and key considerations
This section is written for the person who has to sign off on the integration. Every control below is one you can verify from the outside.
Credentials
Two kinds, and only one moves money
Each shop holds four credentials: a publishable and a secret key, in sandbox and in production. Only secret keys can create charges, refund, read buyer data or send payouts. A publishable key on any of those returns 403 key_kind_not_allowed. The only endpoint that accepts either is GET /ping, which exists so you can validate a credential and has no side effects.
At rest
Keys are stored encrypted with AES-256-GCM, keyed from a master key held in the environment, never in the database. Secrets are truncated in logs (first 10 characters and last 4) and never written to an error record.
Tokens
The bearer token is HS256, signed with a key derived specifically for this purpose. Verification recomputes the HMAC unconditionally and never reads the alg header, so algorithm-confusion and alg:none are not reachable. Comparison is constant-time.
Isolation
Environment
The key decides the environment, and the seal is hard in both directions: a sk_test_ against a shop in production is refused with 403 environment_mismatch, and vice versa. There is no header, flag or body field that overrides it. Twenty-four tables carry the environment as a partition column.
Tenant and shop
A shop-scoped key cannot read a sibling shop's transaction. Fetching one returns 404, not 403 — deliberately, so the response cannot be used to confirm that a record exists under another shop. A merchantId in the request body is only honoured when it matches the authenticated one; it can never widen scope.
Webhook signatures
Every delivery carries X-Key2Pay-Signature: t=<unix>,v1=<hex>, where the MAC is HMAC-SHA256 over <timestamp>.<raw body>. The timestamp is inside the MAC, so it cannot be altered without invalidating the signature.
const [, ts, sig] = /t=(\d+),v1=([0-9a-f]+)/.exec(header);
// 1. Reject anything older than 5 minutes — this is your replay defence.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return reject();
// 2. Recompute over the RAW body. Parsing first and re-serialising
// changes the bytes and the signature will never match.
const expected = crypto.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`).digest("hex");
// 3. Constant-time compare.
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return reject();v1= with the new one and v0= with the previous — so you can deploy the new secret without a coordinated cutover and without dropping a single event.Transport
Webhook URLs are resolved when you register them and refused if they point at a private, loopback, link-local or cloud-metadata range.
Security controls and limitations
Stating this plainly is more useful to your security review than discovering it during certification.
| Control | State |
|---|---|
| IP allowlisting on API keys | Available per shop for production secret keys and their access tokens. Enforcement checks your server's egress IP against registered IPs or CIDR ranges. Sandbox and publishable keys are excluded. See the IP allowlist guide for observation, enforcement and exemptions. |
| Per-day / per-month velocity limits | Not available. Limits are per transaction (min/max per method). |
| Token revocation | Refreshing does not revoke previously issued tokens; they remain valid until expiry. Rotating the API keys increments the credential version (kv) and immediately revokes every access and refresh token minted from the previous version. |
| Refund executed by us | A refund opens a dispute; the money is returned by the provider on the original rail, not by us. |