Security
This chapter consolidates the integration-security guidance that is scattered across the Authentication, Sandbox, Errors, and Callbacks chapters. Read it when you are preparing to go live, when you are running a security review, or when an auditor asks what Payment Gateway guarantees on the wire and what you are expected to do on your side. Security is a shared-responsibility model: Payment Gateway hardens the platform; you harden the integration; neither side alone is sufficient.
Threat model
The following table enumerates the threats the merchant API surface is designed to resist, and where the responsibility for each mitigation sits. "Both" means a control exists on each side and dropping one is not safe.
| Threat | Mitigation | Owner |
|---|---|---|
| Replay attack on inbound requests | X-Timestamp ±5 minute window enforced server-side; signature alone does not stop replay. | Payment Gateway |
| Replay attack on inbound callbacks | Same ±5 minute window applied by your callback handler; combine with (order_no, status) idempotency. | Merchant |
| Body tampering on inbound requests | X-Signature required on every request; verify_md5_signature runs before any business logic. | Payment Gateway |
| Body tampering on inbound callbacks | X-Signature verified by your callback handler before any side effect is applied. | Merchant |
| IP spoofing into the API | Source-IP control at the Cloudflare edge (WAF / Access). | Payment Gateway |
Credential leak (api_key) | Rotation procedure available on request; you store keys in a secret manager and redact in logs. | Both |
| DNS poisoning / MITM | TLS 1.2+ required end-to-end; gateway rejects HTTP callback URLs and self-signed merchant certificates in production. | Both |
| Brute-force credential guessing | Cloudflare edge rate-limits the API hostnames; you should rate-limit your own auth failures. | Both |
| Stolen log file leaking credentials | Log redaction patterns documented below; you implement them in your logger. | Merchant |
| Concurrent replay racing your handler | Idempotency keyed on (order_no, status); deduplicate before applying side effects. | Merchant |
Nothing on this list is purely the gateway's problem. Even controls owned by Payment Gateway have a merchant-side analogue you cannot skip.
Credential lifecycle
Issuance
Your api_key is provisioned by your account manager during onboarding. You receive two keys: one for sandbox, one for production. The two are independent — a sandbox key cannot be used against production, and vice versa. The key is a 32-character hex token generated by secrets.token_hex(16) on the platform side and delivered out of band.
Rotation triggers
Rotate immediately when any of the following occurs:
- A key is known or suspected to be exposed (committed to source control, pasted into a ticket, logged in plaintext, sent over an untrusted channel).
- An employee with key access leaves or moves to a role without a need-to-know.
- A vendor or integrator that held the key offboards.
- Scheduled rotation cadence comes due. The recommended minimum cadence is quarterly.
How to rotate
Rotation is operator-mediated. Contact your account manager. The flow is:
- Account manager provisions a second active key against your merchant record.
- You deploy the new key alongside the old one and verify traffic flows.
- Account manager retires the old key after a brief overlap window (approximately 5 minutes) during which both keys are accepted, allowing a hot cutover without dropped requests.
There is no merchant-portal self-service rotation today. Plan for the operator handoff in your incident-response runbook.
Storage
api_key is credential-equivalent to a long-lived password. Store it accordingly:
- Use a secret manager: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or equivalent.
- Inject at runtime from the secret manager into your application's process environment or memory.
- Never commit to source control, even in encrypted form. Encryption at rest on the secret-manager side is not equivalent to "encrypted in the repo" — the latter is recoverable from any historic clone.
- Never bake into a container image; image layers leak.
- Never ship in client-side code, mobile apps, or anything served to a browser. The merchant API surface is server-to-server only.
Distribution within your team
Limit knowledge of the production api_key to operations and on-call engineers. Use just-in-time access where your platform supports it. Audit every direct read of the secret. Treat a "who knows the production key" list as something you review at the same cadence you rotate.
Transport
Outbound (your service → Payment Gateway)
All merchant API hostnames are HTTPS only. The gateway terminates TLS at the edge and rejects plaintext HTTP. The accepted TLS minimum is TLS 1.2; TLS 1.0, TLS 1.1, and SSLv3 are not negotiated. Pin your client to TLS 1.2 or higher; do not configure fallback to older suites.
Inbound (Payment Gateway → your callback URL)
- HTTPS required in production. A
callback_urlthat does not start withhttps://is rejected at order-create time in production. There is no opt-out. - Valid certificate chain required. The gateway validates your TLS certificate against the public CA bundle. Self-signed certificates are rejected (
webhook_ssl_verify = Trueby default). Use a real certificate from any public CA. - TLS 1.2+ on your end. Disable TLS 1.0 and 1.1 on your callback endpoint. The gateway will refuse to negotiate them.
Cipher suites and modern hygiene
Disable export ciphers, RC4, 3DES, and anything labeled "EXPORT" or "anonymous". Enable forward secrecy (ECDHE). Modern defaults from any current TLS library are correct.
Network-layer access control
Inbound requests to the merchant API pass through a Cloudflare-edge source-IP allowlist (Cloudflare Access at the edge). The full model and its failure modes are documented in Sandbox. The same edge control exists in production. Removing yourself from the Cloudflare Access allowlist is a common cause of "it worked yesterday" tickets.
Request integrity
Signature algorithm
Every inbound merchant request and every outbound callback is signed with MD5(sorted_params + api_key), lowercase hex. The full step-by-step algorithm and four reference implementations live in Authentication & Signing. This chapter does not re-derive it.
The choice of MD5 here reflects payment-channel industry convention; it is used as a MAC-style integrity check with a shared secret, not as a collision-resistant hash. Because the api_key is concatenated and never transmitted on the wire, an attacker cannot forge a signature without the key even with arbitrary MD5 collision capability.
A future migration to HMAC-SHA256 may be announced. When it is, it will appear in the Changelog with a stable transition plan, a parallel-acceptance window, and a deprecation date. Do not pre-emptively re-implement; the contract is what is documented here.
Timestamp window
X-Timestamp is Unix epoch seconds. The server-side window is ±5 minutes (300 seconds) of the gateway's clock, enforced before signature verification. Outside the window, the response is 401 INVALID_TIMESTAMP (see Errors).
Mirror this check on your callback handler. The gateway sends a fresh X-Timestamp on every dispatch and every retry; reject any callback whose timestamp is more than 5 minutes off your clock. Sync your clock to NTP — do not "fix" drift by trusting the timestamp the gateway sent you.
Timing-safe comparison
When you verify an inbound callback signature, use a constant-time equality function:
- Python:
hmac.compare_digest(expected, received) - PHP:
hash_equals($expected, $received) - Node.js:
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)) - Go:
subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
Plain string equality (==) leaks the position of the first differing byte through timing variation. With enough samples an attacker can recover a forged signature byte by byte. The gateway uses hmac.compare_digest server-side; you should match that on your side. The verify samples shipped with Authentication use the correct primitive in each language.
Replay within the timestamp window
A valid signature plus a fresh timestamp does not, by itself, prevent replay within the 5-minute window. A network attacker who captures a callback can replay it for up to 5 minutes. The control that defeats this is idempotency by (order_no, status) on the merchant side — covered in the next section. The signature plus timestamp window narrows the replay opportunity; idempotency neutralizes it.
Idempotency as a security property
Duplicate callbacks from legitimate network retries (the gateway re-dispatches the same NotificationLog row up to 5 times) and adversarial replay attempts are indistinguishable from the application layer. Both arrive as a valid signature on an identical body. Both are addressed by the same control: deduplicate on (order_no, status) before applying any side effect.
This is why Callbacks recommends a primary-key table over (order_no, status) and an insert-or-no-op handler. From a security perspective, that table is your replay-defense layer, not just a retry-correctness layer.
Why not other keys:
notification_id— there is no per-attempt event id surfaced in the callback body. The gateway re-uses the sameNotificationLogrow across all retries.X-Timestamp— changes per retry attempt, so deduplicating on it would allow the same event to apply 5 times (legitimately) and any number of times (adversarially).(order_no, status)— stable across retries, distinct across legitimate state transitions (e.g.MATCHEDthenCOMPLETEDare both real and both should be processed).
Logging hygiene
A leaked log file becomes a leaked credential when secrets are logged in plaintext. Apply the following discipline at the logger layer — not at the call site, where it will be skipped.
Do log (with redaction applied)
- HTTP method, request path, response status.
- Envelope
codefrom the response (see Errors). - Your own
merchant_order_noand the gatewayorder_no. notification_type/eventfield from callbacks.- Boolean signature-verify result (
passorfail). - Your internal trace ID or request ID for correlation.
X-Timestampvalue and the skew you observed against your local clock.
Do not log (credential-equivalent or PII)
- Full
api_key. Log only the first 8 characters and an ellipsis:pre_xxxxxxxx.... Never the trailing bytes; never the full string concatenated into a query string. - Full
X-Signaturevalue. Log only that verification passed or failed. The hex value has no debugging utility — if it is wrong, you cannot recover what it should have been from the log. - Full
bank_card_number. Mask all but the last 4 digits. - Full
account_no. Mask all but the last 4 digits. - PII tuples. Recipient name + bank + account number together identify a real person at a real bank. Mask, omit, or store encrypted under separate access control.
- Raw request body before signature verification. A body you have not yet verified is attacker-controlled. Log it only after
X-Signaturepasses, with the redactions above applied.
Where logs land
- Encrypt at rest. Most managed log stores do this by default — confirm yours does.
- Access-control the log store: read access is a privileged role, not a developer default.
- Retention: a sensible default is 90 days hot, 1 year cold. Match your regulatory environment.
- Backups inherit the same controls. A leaked backup is a leaked log.
SIEM integration
Forward the following as security events to your SIEM:
401responses from the gateway (AUTHENTICATION_ERROR,INVALID_SIGNATURE,INVALID_TIMESTAMP).403responses from the gateway (AUTHORIZATION_ERROR).- Signature-verify failures on your callback handler.
- Any source IP that produces a sustained burst of either.
A pattern of 401s after a clean period typically means clock drift or a partial key rotation. Source-IP drift now manifests at the Cloudflare edge (TLS handshake failures or HTML challenge / block pages, not a JSON 403); a JSON 403 now means a disabled merchant or a missing permission. Both are operationally urgent.
Pitfalls
The mistakes below have shipped to production in real merchant integrations. Each one is paired with the error code or symptom you will see.
- Logging the full
api_keyanywhere. Debug logs, exception reporters, application metrics, error-tracking SaaS payloads. Log only the first 8 characters and an ellipsis. Treatapi_keysubstrings in any log line as a leaked credential and rotate. - Logging the
X-Signaturevalue. The value has no debugging use. Log a boolean pass/fail only. A logged signature, paired with the logged body, leaks the input to a chosen-message attack. - HTTP-only
callback_urlin production. Rejected at order-create time. You will see400 VALIDATION_ERRORor equivalent (see Errors). Use HTTPS. - Self-signed certificate on your callback endpoint in production. Connection refused by the gateway (
webhook_ssl_verify = True). Use a public CA. - Reusing the same
api_keyacross sandbox and production. Not possible by design (sandbox keys are rejected by production and vice versa), but reusing the handling pattern — for example, the same secret-manager entry, the same logger, the same incident path — couples your environments. Keep them separate end-to-end. - Storing
api_keyin source control. Even encrypted, even withgit-crypt, even in a private repo. The encryption layer is downstream of the source-control layer; historic clones survive rotations. - Skipping signature verification on "internal" callbacks. Sandbox callbacks are signed by the gateway with the same algorithm as production. If you skip verification in sandbox, you will ship that code path to production. Verify in both. See Callbacks.
- Branching on
messagetext instead ofcode.messageis informational and may change wording.codeis the stable contract. See Errors. - Hardcoding the 5-minute timestamp window in your client. The window is a server-side policy. If the gateway tightens it, a client that signs with
now() - 4m59swill start failing. Sign withnow()and let the server enforce its own window. - Forgetting timing-safe comparison when verifying signatures. String equality is leaky. Use
hmac.compare_digest,hash_equals, orcrypto.timingSafeEqualdepending on your language. - IP allowlist drift. Your NAT exit IP changes (new datacenter, new CI runner pool, cloud-provider IP refresh) but the Cloudflare Access allowlist does not. Because source IP is enforced only at the Cloudflare edge, you will not see a JSON
403envelope — the connection fails at the edge as a TLS handshake failure or an HTML challenge / block page. Keep the Cloudflare Access allowlist in sync with your NAT pool to reduce drift. See Sandbox — the same model applies in production. - Storing PII (
bank_card_number,account_no, recipient names) unencrypted in your callback persistence layer. Encryption at rest is not equivalent to encryption in your database column. If your operators or your read replicas see plaintext, your audit posture is wrong. - Trusting CDN edge headers without validating the chain.
X-Forwarded-Foris appended by every proxy in the chain. The first IP in the list is whatever the immediate client wrote; only the rightmost IPs (added by your trusted proxies) are authoritative. Validate by counting trusted hops, not by trusting the leftmost value. - Forgetting to rotate
api_keyafter an employee with key access leaves. Rotation is part of offboarding, not a quarterly afterthought. The offboarding runbook must include it. - Catching signature-verify exceptions and continuing. A try/except around the verify call that swallows the failure and processes the body is a privilege-escalation vector. Return
401and stop. See Callbacks. - Logging the raw request body before verification. Attacker-controlled content lands in your log store, including arbitrary JSON keys that may pattern-match against log-redaction rules and let secrets slip through. Verify, then log.
Going-live security checklist
Before you flip your production deployment to point at api.open-pay.co, confirm every item:
- [ ] Callback URL is HTTPS and the certificate chain validates against a public CA.
- [ ]
api_keylives in a secret manager, not in an env file in the repo, not in an image layer, not in client-side code. - [ ] Signature verification is on the request path for every callback handler. Not behind a feature flag, not skipped for "internal" environments, not bypassed when a debug header is set.
- [ ] Idempotency table or cache keyed on
(order_no, status)is deployed and tested with a duplicate-callback scenario. - [ ] Log redaction tested. Grep your staging logs for
api_key=pre_,X-Signature:, and other credential patterns. The grep should return zero matches. - [ ] IP allowlist matches your production NAT exit IPs, on Cloudflare Access. Documented, with an owner, and reviewed quarterly.
- [ ] SIEM receives auth failure events: 401 / 403 from the gateway, signature-verify failures on your handler.
- [ ] Key rotation cadence agreed with your account manager. Quarterly minimum. The rotation runbook is written and someone other than the author has read it.
- [ ] Incident response runbook covers the leaked-
api_keyscenario. Who you call, what you ask for, how long the overlap window is, how you confirm the old key is dead. - [ ] Sandbox and production credentials are clearly separated in your deployment config. Different secret-manager paths, different environment variables, different log prefixes. No "fall-through" defaults that pick sandbox when production is unset.
- [ ] Clock sync (NTP) is configured on every host that signs requests or verifies callbacks. Drift more than 5 minutes is a hard failure.
- [ ] TLS 1.2+ enforced on both directions. No fallback to 1.0 / 1.1.
Cross-references
- Authentication & Signing — full signing algorithm, the 5-minute timestamp window, and the four-language reference implementations.
- Sandbox — Cloudflare edge source-IP allowlist model (Cloudflare Access) and how to recognize an edge rejection.
- Errors — security-related error codes (
401 INVALID_SIGNATURE,401 INVALID_TIMESTAMP,403 AUTHORIZATION_ERROR) and the response envelope contract. - Callbacks — callback signature verification, idempotency by
(order_no, status), and the retry policy. - Quickstart — the 15-minute path from first request to first verified callback.
- API Reference — full endpoint listing, request and response schemas.