Trust the webhook, never the redirect
Customers can bookmark, share, or be redirected by a flaky browser. The webhook is the only ground truth. Don't fulfil an order on return_url alone.
Validate webhook signatures
Every webhook is HMAC-SHA256 signed with your endpoint secret over the timestamp and the raw body. Reject any delivery whose signature doesn't match or whose timestamp is more than 300 seconds old — without this check, anyone who knows your URL can mark orders paid.
Make the webhook handler idempotent
We retry deliveries on non-2xx responses, and at-least-once means you will sometimes get the same event twice. There is no separate event id: store data.id together with event under a UNIQUE constraint and ignore a pair you have already handled. Check data.amount against your own order before you mark it paid.
Use an idempotency key when you create a payment
Generate a fresh UUID per checkout attempt and save it with the order. On retries of that attempt, send the same key — the server returns the original response instead of creating a second payment.
Keep secrets in a vault, not in env files committed to git
At minimum, use a .env file that's in .gitignore. Better, use your platform's secret manager (AWS Secrets Manager, Doppler, 1Password). Rotate on every offboard.
Use the sandbox to test failure modes
Don't wait for production to discover what happens when a customer cancels mid-flow. The sandbox lets you finish a test payment as succeeded, failed or cancelled from your server.
Cap your timeouts
Set a connect timeout of 5s and a read timeout of 30s. Anything longer is almost certainly a hung connection — fail fast and let your retry loop handle it.