> For the complete index of the 402pay docs, see [llms.txt](https://developer.402pay.co/llms.txt).

# Security best practices

Narrow keys, rotations without downtime, a locked-down webhook endpoint and a secure account.

Your business is reached through your team's email and password sign-in, API keys, webhook signing secrets and, for a wallet made here, a recovery phrase. Keep each one as narrow as it can be, rotate it without downtime, and notice when anything changes.

## Least-privilege keys

A secret key has full access, or a level per resource: none, read or write. Give each integration its own key with only what it uses.

| Integration | Access |
| --- | --- |
| Checkout server that creates payments and reads them back | Payments: write |
| Webhook handler | None to verify, which uses the endpoint's secret. Payments: read if it fetches the payment. |
| Sweep for missed events | Events: read, Payments: read |
| Accounting export | Payments: read, Wallet: read |
| Link manager or AI agent | Links: write, Payments: read |

- `GET` requests need read, and every other method needs write, which includes read.
- Resources you leave out get none, and a restricted key needs access to at least one.
- Events can only be read, and reading one also needs read on its subject's resource, since the event carries a snapshot of it. Asking for `include=customer` on payments needs read on customers.
- A key's access changes only in the dashboard, so no key can widen its own. A key that falls short gets 403 `permission_denied`, naming what it lacks.

## Rotate a secret key

Keys don't expire, and a new key works alongside the old one until you revoke it, so rotating takes no downtime.

1. Create a new secret key with the same access, under Developers, then API keys.
1. Deploy it everywhere the old key is used.
1. Watch the old key's Last used time in the dashboard until it stops moving.
1. Revoke the old key. It stops working at once, and revoking it again changes nothing.

A revoked key gets 401 `invalid_api_key`. Only a hash of each secret is stored, so a lost secret can't be shown again: rotate instead. Rotate whenever someone with access leaves, and at once if a key may have leaked into a commit, a log or a screenshot.

## Rotate a webhook secret

Rotate an endpoint's signing secret in the dashboard or with [`POST /webhooks/{id}/rotate-secret`](https://developer.402pay.co/api/webhooks/rotate-secret.md). The response shows the new secret once. For the next 24 hours every delivery carries a signature from each secret, so deploy the new one within that window and nothing fails in between.

Rotate a signing secret, cURL:

```bash
curl -X POST "https://dash.402pay.co/api/v1/webhooks/whk_GFAKLrE8wkBwF4WL/rotate-secret" \
  -H "Authorization: Bearer $PAY402_SECRET_KEY"
```

Rotate a signing secret, Node.js:

```js
const response = await fetch("https://dash.402pay.co/api/v1/webhooks/whk_GFAKLrE8wkBwF4WL/rotate-secret", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAY402_SECRET_KEY}`,
  },
});
const { data } = await response.json();
```

Rotate a signing secret, Python:

```python
import os

import requests

response = requests.post(
    "https://dash.402pay.co/api/v1/webhooks/whk_GFAKLrE8wkBwF4WL/rotate-secret",
    headers={
        "Authorization": f"Bearer {os.environ['PAY402_SECRET_KEY']}",
    },
)
data = response.json()["data"]
```

## Protect your webhook endpoint

- Verify every delivery's signature over the raw body, compare in constant time, and refuse a `webhook-timestamp` more than five minutes off. The [framework recipes](https://developer.402pay.co/guides/frameworks.md) do all three.
- Endpoint URLs must be public `https://` addresses. `http://`, `localhost` and private addresses are refused.
- Skip events with `test: true`, and any `webhook-id` you've already handled.
- An event's `data` is a snapshot. Before you ship something valuable, fetch the payment with `GET /payments/{id}` for its state now.
- Keep the signing secret with your other secrets, never in source control.

## Requests and responses

- A key already names its business, so leave out the `402pay-Business` header. Naming another business with it returns 403 `business_forbidden`.
- Send the key exactly as `Authorization: Bearer 402s_…`. Any other shape, and any publishable `402p_` key, returns 401 `invalid_api_key`.
- Responses that carry a secret, such as a new webhook endpoint or a rotated secret, are sent with `Cache-Control: no-store` and never replayed for an `Idempotency-Key`, so a retry makes another one. If creating an endpoint times out, list your endpoints before you try again.
- Log the `402pay-Request-Id` header with every error, so support can find the request.

## Watch for changes

Subscribe an endpoint to `api_key.created`, `api_key.updated`, `api_key.revoked`, `webhook.created`, `webhook.updated` and `webhook.deleted`, and alert on any you didn't expect. Each event's `actor` says who made the change. API key events are covered by no permission, so subscribe to them from the dashboard or with a key that has full access. The audit log, in Settings, shows what each person on your team did.

## Secure your account

- Sign in with a passkey: add one in Settings, under General, then choose Continue with Passkey. A passkey works only on 402pay's own site, so a lookalike page can't phish it, and there's no password or code for anyone to steal. Unlocking it with your device's fingerprint, face or PIN counts as both steps, so it never asks for a two-step code.
- If you also use a password, make it at least 15 characters and don't use it anywhere else. A password manager can make one, and a few unrelated words work well too.
- Turn on two-step verification in Settings, under General, which asks for your password first and signs out your other devices. Each code works once, and after five wrong codes, counted across sign-in attempts, codes are locked for 15 minutes with 429 `too_many_attempts`.
- Keep the ten recovery codes apart from your authenticator. Making new ones voids the old set.
- Change your password in Settings, under General, if someone may have seen it. Your other devices are signed out.
- If you forget it, reset it by email from the sign-in page. The link works once, for 30 minutes, two-step verification still asks for a code, and every device is signed out. The email lists API keys and webhooks added in the last month, so you can revoke any you don't know.
- Act on our security emails. We email you about sign-ins from a new device, passkeys added or removed, and changes to your email, password, two-step verification and wallet, and they can't be turned off. When your email changes, the old address gets a link that undoes it for 7 days. An undo also turns off API keys and webhooks added since the change, removes every passkey, and holds password resets for a day so the newer address can object.
- Expect to confirm your password or a passkey before changing the wallet, API keys, webhooks, passkeys or a business, unless you signed in within the last 15 minutes. A stolen session alone can't redirect your payments.
- Once you've been paid, deleting your wallet waits 24 hours, and the email about it has a link that cancels it, so nobody can quietly swap in a wallet of their own.
- Review signed-in devices in Settings, under General, and sign out any you don't know. Sessions end after 14 days unused, or 30 days at most.

## Next steps

- [Authentication](https://developer.402pay.co/authentication.md): Authenticate with a secret key, scope it per resource, and keep it safe.
- [Going live](https://developer.402pay.co/going-live.md): Everything to check before you take real payments, and after.
- [Webhook event catalog](https://developer.402pay.co/guides/webhook-events.md): Every event type, what its data holds, and the order a payment's events arrive in.
- [Wallet and deposits](https://developer.402pay.co/guides/settlement.md): The three kinds of wallet, how each payment lands in yours, and who holds the keys.
