Security
Apa is non-custodial — it never holds your funds or private keys. Customers sign from their own wallet and payouts settle straight to yours. Your responsibilities are narrow: protect your secret key and verify every webhook.
API keys
Each environment uses secret API keys for server-side calls. A secret key must never reach the browser, a git repo, or a bundle. Keys are shown once at creation — Apa stores only a hash, so a lost secret can't be recovered, only rotated.
Client-side code should use hosted checkout URLs or public checkout/session tokens returned by your backend, not an API key.
Saved payout wallets
This is the property that makes a leaked key far less dangerous than on most payment APIs. Live checkout sessions and payment links settle to a saved payout_wallet_id (pp_…) — never a raw wallet address passed in the request. The destination is fixed by the payout wallet you saved in the dashboard, not by the request body.
So even if an attacker obtains your secret key, they cannot redirect a payout to their own wallet. The worst they can do is create sessions that pay you.
# Good — reference a saved payout wallet by id
curl https://apa.app/v1/checkout/sessions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"payout_wallet_id":"pp_123","amount":"100.00","currency":"USD"}'
# Rejected — raw wallet addresses are not accepted for live payments
# { "payout_address": "0xattacker…" } ← never honoredWebhook signatures
Anyone can POST JSON at your endpoint, so verify before you trust. Every Apa webhook carries an Apa-Signature header of the form t=<timestamp>,v1=<hex>, where v1 is an HMAC-SHA256 over <timestamp>.<raw body> keyed by your endpoint's whsec_ secret. Recompute it, compare in constant time, and (optionally) reject stale timestamps to prevent replay.
// Apa-Signature: t=<timestamp>,v1=<hex>
const parts = Object.fromEntries(
req.header("Apa-Signature").split(",").map((p) => p.split("="))
);
const expected = crypto
.createHmac("sha256", process.env.APA_WEBHOOK_SECRET) // whsec_…
.update(`${parts.t}.${rawBody}`) // "<timestamp>.<raw body>", before JSON.parse
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))) {
return res.status(400).send("invalid signature");
}
// Optional replay protection: reject if the timestamp is too old.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
return res.status(400).send("stale timestamp");
}Non-custodial by design
Apa orchestrates payments but never takes custody of funds and never stores private keys. Customers sign every transaction from their own wallet, and settled funds go straight to the merchant's wallet — Apa is never a stop along the way. There is no pooled balance to compromise and no key vault to breach.
The trade-off is that on-chain transfers are final: validate the receive address when you create a payout wallet, and confirm payment state server-side — via webhook or a GET /v1/checkout/sessions/:id call — before fulfilling. Never trust a client-side redirect alone.
Best practices
- Reference saved payout wallets (pp_…) for live payments — never raw wallets.
- Keep sk_live_ keys out of source control and client bundles; load them from a vault or env vars.
- Rotate keys on a schedule and after any suspected exposure — reveal is one-time.
- Verify every webhook signature and make handlers idempotent.
- Confirm payment state server-side before fulfilling an order.
- Validate receive addresses; treat on-chain transfers as final.
- Return a 2xx quickly and do heavy work asynchronously.