Skip to content

Authentication

Every request carries an API key in a header. There are no OAuth flows, no signed requests and no session cookies: one key, one header, server-side only.

Both are equivalent. Send one.

Terminal window
# Bearer token
curl https://api.vaki.cohttps://public-api-staging.vaki.co/v1/vakis/clinicalaliga \
-H "Authorization: Bearer $VAKI_API_KEY"
# X-API-Key
curl https://api.vaki.cohttps://public-api-staging.vaki.co/v1/vakis/clinicalaliga \
-H "X-API-Key: $VAKI_API_KEY"

Two details that cost people time:

  • The scheme is Bearer, capital B, one space. bearer, BEARER or Token are not recognised and you get a 401 that reads like a bad key.
  • Authorization wins. If you send both headers, the Authorization header is the one read. A stale Authorization header from a shared HTTP client will silently shadow the X-API-Key you thought you were sending.

Query parameters are not accepted. There is no ?api_key=. Keys in URLs end up in access logs, browser history, Referer headers and screen shares, so the API refuses to read them at all.

Do not put an API key in a browser, a mobile app, a public repo or a front-end build. Anyone holding the key can create causes and mint checkout links as you.

The API’s CORS policy permits requests from HTTPS origins so that server-side proxies and tooling work; that is not an invitation to call it from a page. The integration shape that keeps you safe is the one in Accept a donation from your own site: your browser talks to your server, and only your server holds the key.

Treat the key as an opaque string. Store it in a secret manager or an environment variable, pass it through verbatim, and never parse, split or truncate it.

Keys are generated as 32 bytes of cryptographic randomness, prefixed for recognisability, and stored only as a SHA-256 hash. Practical consequences:

  • The raw key is shown exactly once, at issue time. Vaki cannot recover it for you — a lost key is replaced, not retrieved.
  • Because we only hold a hash, a leaked key is remediated by revoking it, not by asking us to check whether it was used.
  • The prefix is for humans and secret scanners. Do not branch on it.

Live keys are published as vk_live_…. Every example on this site uses $VAKI_API_KEY rather than a literal prefix, because the prefix is not something your code should ever look at.

A key carries an explicit list of permissions. A route declares the permissions it needs, and all of them must be present on your key — it is an AND, not an OR. A missing permission is a 403, not a 401:

{
"type": "https://developers.vaki.co/errors/forbidden",
"title": "Forbidden",
"status": 403,
"code": "forbidden",
"detail": "Insufficient permissions",
"instance": "/v1/checkout_links"
}

Ask for the narrowest set that does the job:

PermissionLets you
checkout_links:writeMint checkout links
checkout_links:readRead a checkout link’s status
vakis:writeCreate causes
vaki:readRead causes

Permissions for the Coming soon endpoints are deliberately not issued yet. When bank accounts and withdrawals land they will each carry their own permission — a key that can take donations will never implicitly gain the ability to move money out.

A key may be issued with an expiry date. Past it, every request fails with a 401 and code: "unauthorized"; the detail member says the key expired, which is where your alerting can tell “we forgot to rotate” apart from “someone is probing us”. Keys issued without an expiry do not expire on their own; they end when they are revoked.

Revocation takes effect on the next request. There is no cache to wait out and no propagation delay. Revoke immediately if a key is exposed — in a log, a screenshot, a repo, a Slack thread, a support ticket.

Multiple keys can be active for the same client at the same time, which is what makes zero-downtime rotation possible. There is no self-service rotation endpoint yet; rotation is a request to Vaki plus a deploy on your side:

  1. Ask Vaki for a second key with the same permissions. Say it is a rotation, so nobody revokes the first one on your behalf.
  2. Deploy the new key to your secret store. Both keys are now valid, so there is no window where requests fail.
  3. Confirm the old key is idle. Vaki records a lastUsed timestamp per key and can tell you whether anything is still calling with the old one — worth asking before step 4 if you have workers or cron jobs you might have missed.
  4. Revoke the old key.

Rotate on a schedule you choose, and immediately on any suspected exposure or when someone who had access to the key leaves.

A key can be pinned to a list of source IP addresses. When the list is set, requests from any other address are rejected with a 403, whether or not the key is valid. When it is empty, the key works from anywhere.

An allowlist miss and a missing permission both return code: "forbidden", and the API deliberately does not tell you which of the two tripped — saying so would tell an attacker which check it cleared. So if you enable an allowlist, you own knowing that you did.

This is genuinely useful if your calls come from static egress, and genuinely painful if they do not. Before asking for it, read the constraints:

  • Exact string matches only. CIDR ranges are not supported. 10.0.0.0/8 will never match anything. Every address must be listed individually.
  • The address is taken from the first entry of X-Forwarded-For, falling back to X-Real-IP and then the socket address. It must match the address your traffic presents to Vaki’s load balancer — not your office IP, not your developer laptop.
  • IPv6 counts as a different address. If your egress can emit either family, list both, or you will get intermittent 403s that look like a Vaki outage.
  • Serverless and autoscaling platforms rotate egress IPs. On Cloud Run, Lambda, Vercel or Heroku without a static NAT, do not use an IP allowlist. Pin a NAT gateway first, or rely on permissions and rotation instead.

If you enable it, keep one key without an allowlist, held by a human, so an IP change cannot lock you out of your own integration.

StatuscodeCausedetail tells you
401unauthorizedNo Authorization: Bearer and no X-API-Key headerthat a key is required
401unauthorizedThe key matches no active keythat the key is invalid
401unauthorizedValid key, past its expiry datethat the key expired
403forbiddenA missing permission or an IP-allowlist misswhich one — not by design

All three 401 conditions share one code, because a client’s correct reaction to all three is identical: stop, alert a human, fix the credential. The distinction lives in detail, which is for your logs and is not stable enough to branch on.

All of them are problem documents. Branch on code.