The three kinds of key
A key is minted in the dashboard and returned exactly once. Only a selector and a hash of the verifier are stored, so there is no reveal endpoint and there never will be.
zk_sk_live_…Secret key
Server-side only. Full merchant authority, and optionally bound to one wallet so a key separates one project’s money from another’s.
zk_pk_live_…Publishable key
Safe in a browser or an app, but it cannot create a payment or read your data. It lists the methods available for a corridor and, with a payment’s client secret, confirms and follows that one payment.
zk_rk_live_…Restricted key
Server-side, limited to an explicit list of permission scopes. One per integration, so a compromise has a blast radius rather than a total.
Which key for which call
Every endpoint, and what each kind of key needs to call it. A restricted key needs the scope shown; a secret key needs nothing extra.
| Endpoint | Secret | Restricted — scope | Publishable |
|---|---|---|---|
| POST /v1/payment_intents | Yes | payments.intents.create | No |
| POST /v1/payment_intents/:id/confirm | Yes | payments.intents.create | With the client secret |
| GET /v1/payment_intents/:id | Yes | payments.intents.read | With the client secret |
| GET /v1/payment_intents | Yes | payments.intents.read | No |
| POST /v1/customers | Yes | customers.write | No |
| GET /v1/providers | Yes | Any key | Yes |
| POST /v1/payment_links | Yes | billing.links.manage | No |
| GET /v1/payment_links/:id | Yes | billing.links.read | No |
| POST /v1/payment_links/:id/deactivate | Yes | billing.links.manage | No |
Using a publishable key
For a checkout you build yourself. Your server creates the payment with the secret key and gives the browser two things: the payment id and its clientSecret. The browser then confirms it and follows its progress with the publishable key, sending the client secret each time. The key is public by design; the client secret is what limits it to the one payment it was handed.
// 1. Your server creates the payment with the SECRET key, and hands the
// browser only the id and the client secret.
app.post('/pay', async (req, res) => {
const response = await fetch('https://api.zintle.io/v1/payment_intents', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZINTLE_SECRET_KEY}`,
'Idempotency-Key': `order-${req.body.orderId}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: '500000',
currency: 'XAF',
country: 'CM',
methodType: 'MOBILE_MONEY',
reference: req.body.orderId,
customer: req.body.customerId, // phone and email come from here
}),
});
const intent = await response.json();
res.json({ id: intent.id, clientSecret: intent.clientSecret });
});Scopes
A restricted key carries permission codes and may exercise only the intersection of its own scopes and what its merchant is allowed to do. Widening a merchant’s permissions never silently widens a key.
Create one under Developers → API keys: choose Restricted, then tick the scopes. You can only grant scopes you hold yourself. The key works exactly like a secret key for everything in its scopes, and answers 403 for everything else.
# A key for your storefront server that can create checkouts and nothing else:
# scopes billing.links.* — no refunds, no customer list, no payment history.
curl https://api.zintle.io/v1/payment_links \
-H "Authorization: Bearer zk_rk_test_7d60534f2a9c1e8b" \
-H "Content-Type: application/json" \
-d '{ "amount": "500000", "currency": "XAF", "title": "Order #4471", "reference": "order-4471" }'Test and live
The environment is encoded in the key itself — zk_sk_test_… or zk_sk_live_… — and is never inferred from a header, a hostname or a request field. A test object is unreachable from a live key and the reverse.
IP allowlist
Each key can name the source addresses permitted to use it. It defaults to empty, meaning any address, because a merchant on dynamic egress would otherwise lock themselves out — but on a fixed egress it is the strongest single control you have.