Documentation
Authentication
API keys, scopes, and request signing.
JuicePay authenticates with bearer API keys. Keys are environment-scoped and carry a set of scopes that bound what they can do.
Send a key#
curl -s "$JUICEPAY_URL/v1/balances" \
-H "Authorization: Bearer $JUICEPAY_KEY"A key that is missing, malformed, revoked, or issued for the other environment returns 401 unauthenticated. A key that is valid but lacks the required scope returns 403 permission_denied.
Key formats#
| Prefix | Environment | Example |
|---|---|---|
jp_test_ | Sandbox | jp_test_9f2KxQ7mLp4Vn8Rt |
jp_live_ | Production | jp_live_4mW8bZ2nHq6Yc1Pd |
Prefixes exist so a leaked key is identifiable in a log scanner. Treat a production key in a public repository as compromised and rotate it immediately — revocation takes effect within seconds.
Scopes#
Grant the narrowest set that works. Scopes are space-separated and readable from the key's metadata.
| Scope | Grants |
|---|---|
balances:read | Read balances and ledger entries |
payment_intents:write | Create and update inbound payments |
payouts:write | Create single payouts and batches |
swaps:write | Quote and execute conversions |
webhook_endpoints:write | Register, update and replay endpoints |
reports:read | Download settlement reports |
A checkout service typically needs payment_intents:write and balances:read and nothing more. It should not hold payouts:write; if your checkout service is ever compromised, outbound payments should be impossible.
Read-only keys#
For dashboards, internal tooling, and anything a human holds, mint a read-only key:
curl -s -X POST "$JUICEPAY_URL/v1/api_keys" \
-H "Authorization: Bearer $JUICEPAY_KEY" \
-d '{ "name": "analytics-readonly", "scopes": ["balances:read", "reports:read"], "read_only": true }'Restricted keys by IP#
{
"name": "ci-deploy",
"scopes": ["payment_intents:write"],
"allowed_ips": ["203.0.113.0/24", "198.51.100.7"]
}Requests from outside the list are rejected before authentication is evaluated, and the attempt appears in your audit log.
Rotating keys#
Rotation is two-phase so you never have a window with no working key.
- Create a replacement key with the same scopes.
- Deploy it to every consumer and confirm traffic has moved by watching
last_used_at. - Revoke the old key.
curl -s -X DELETE "$JUICEPAY_URL/v1/api_keys/key_old123" \
-H "Authorization: Bearer $JUICEPAY_KEY"Never authenticate from a browser or mobile client with a secret key. Use a payment intent and its client secret, which is scoped to a single payment and cannot be replayed elsewhere.
Request signing (optional)#
Accounts handling large volumes can require signed requests. Sign the timestamp and the raw body:
import { createHmac } from "node:crypto";
const timestamp = Math.floor(Date.now() / 1000);
const payload = `${timestamp}.${rawBody}`;
const signature = createHmac("sha256", secret).update(payload).digest("hex");
await fetch(url, {
method: "POST",
headers: {
"JuicePay-Timestamp": String(timestamp),
"JuicePay-Signature": `v1=${signature}`,
},
body: rawBody,
});Signed requests are rejected if the timestamp is more than five minutes old, which bounds the replay window.