Quản lý
Keys and billing
Public and secret keys, allowed origins, the beta usage limit, how you're billed, budgets and the key format.
Public and secret keys
A new project comes with one public and one secret key, and the API keys page makes more, each with a snippet that has the key and its endpoint filled in.
| Key | Prefix | Where | Used for |
|---|---|---|---|
| Public | pk_ | Browser code | start({ publicKey }) in the agent |
| Secret | sk_ | Your server only | The Auth-API-Key header of the server API |
A public key ships in your pages' source, so the dashboard keeps showing it. A secret key is stored as a SHA-256 hash only and shown in full once, when you create it; after that the dashboard lists it by a masked preview like sk_4fT9…5hJ0. A key can be disabled, given an expiry date or a monthly spend limit on its own page; a disabled or expired key answers like an unknown one. Checking a key's format offline is in the key format reference at the bottom.
Rotating a key
To replace a leaked secret key, create a new one on the API keys page, deploy it, then revoke the old one. A revoked key stops working right away.
Allowed origins
A public key ships to every browser, so the project's allowed origins decide which sites may identify with it. Set them on the dashboard's Settings page, one per line, like https://example.com. Identification is for web pages only: every request needs the page's Origin, and once the list has an origin it must be on it. While the list is empty identification is accepted from any site, so anyone can identify with your public key from their own site and spend your identifications: add your sites, your development origin included, to lock it down before going live. The dashboard warns while it's empty.
- Paste what you have: a page address keeps only its origin (the path, query and trailing slash are dropped), the scheme and host are lowercased, a default port is left out, and a bare host gets
https://(http://forlocalhost). - Each scheme, host and port is its own origin. Add your development origins too:
http://localhost:5173andhttp://127.0.0.1:5173are two, and each port is its own. - The /demo test page runs on the agent's own origin, so add that while you test with it.
A request from an origin not on the list, or without a page origin at all (a server, an app, or a null origin from a sandboxed frame or a local file) gets 403 { "error": "origin_not_allowed" }, whose message says which and names the origin to add, and the agent rejects with origin_not_allowed. Nothing is counted or billed for it.
Usage limit during the beta
During the beta each project may make 20,000 identifications in total, not per month, and they're free. The Billing page shows what's left. Ask us to raise the limit; from then on the project is billed as below. When the limit is reached, identifications are capped until it's raised.
Billing
Once the usage limit is raised, it's pay as you go: after each monthly billing cycle you're billed for the identifications you used, with no plans or commitments. The first 20,000 identifications of every month are free. Each identification counts once: an event made by a get() that returned a fresh requestId. Cached answers, capped answers and errors aren't billed.
A project is billed in its organization's currency: euros, US dollars, pounds or one of 23 others. Each currency has its own price list. The currency is set on the Organization page, and a change applies to every project in it. The dashboard's Usage page shows this month so far and an estimate of its bill.
Billing isn't live yet: the prices on the pricing page are estimates.
Budgets and limits
So a bug or an attack can't run up a bill unnoticed, four things can stop identifying. Each answers the same quiet { capped: true }:
- Budgets: a daily, weekly or monthly amount for the project, or for all of an organization's projects together, set on the Billing page (Billing › Organization budgets for the organization's). A day's or week's spend is its share of the month's bill.
- A key's monthly spend limit: set on the key's page under API keys, priced like your bill with the free 20,000 included.
- The usage limit during the beta, above.
- The per-IP rate limit: one IP may identify 120 times a minute with one public key, refilled evenly, an IPv6 /64 counting as one IP. It guards against scripts; offices and mobile networks share IPs, so it's generous. Rate limited requests don't count as throttled on your usage page.
Spend only counts past the free monthly volume: the first 20,000 identifications of a month cost nothing, so they spend nothing from a budget or a key's limit, and even a budget or limit of 0 lets them through. It caps from the 20,001st identification of the month on.
When one is reached, nothing is identified or billed until it resets, and your users never see an error: /identify answers 200 { "capped": true }, and the agent's get() resolves with { capped: true } instead of a request ID. Your server then gets no request ID: No request ID says how to handle it.
Reference: key format
You don't need this to use the keys. It's for leak scanners and for checking a key before calling the API.
Keys follow GitHub's token design with Stripe's prefixes: pk_ or sk_, 30 random characters, then a 6 character checksum. The checksum is the CRC32 of the prefix and the random part (the first 33 characters, as UTF-8), written in base 62 with the alphabet A–Z, a–z, 0–9 in that order, and padded on the left with A to 6 characters.
- A mistyped or made up key is refused without a lookup.
- A public key can't pass as a secret one.
- Leak scanners can match
\b(pk|sk)_[A-Za-z0-9]{36}\band confirm a hit with the checksum.
import { crc32 } from 'node:zlib' // Node 22.2 and up
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const isWellFormedKey = (key: string) => {
if (!/^(pk|sk)_[A-Za-z0-9]{36}$/.test(key)) return false
let value = crc32(key.slice(0, -6))
let checksum = ''
while (value > 0) {
checksum = ALPHABET[value % 62] + checksum
value = Math.floor(value / 62)
}
return checksum.padStart(6, 'A') === key.slice(-6)
}import re
import zlib
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
def is_well_formed_key(key: str) -> bool:
if not re.fullmatch(r"(pk|sk)_[A-Za-z0-9]{36}", key):
return False
value = zlib.crc32(key[:-6].encode())
checksum = ""
while value > 0:
value, digit = divmod(value, 62)
checksum = ALPHABET[digit] + checksum
return checksum.rjust(6, "A") == key[-6:]function isWellFormedKey(string $key): bool
{
if (!preg_match('/^(pk|sk)_[A-Za-z0-9]{36}$/', $key)) {
return false;
}
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$value = crc32(substr($key, 0, -6)); // unsigned on 64-bit PHP
$checksum = '';
while ($value > 0) {
$checksum = $alphabet[$value % 62] . $checksum;
$value = intdiv($value, 62);
}
return str_pad($checksum, 6, 'A', STR_PAD_LEFT) === substr($key, -6);
}const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
var keyPattern = regexp.MustCompile(`^(pk|sk)_[A-Za-z0-9]{36}$`)
func IsWellFormedKey(key string) bool {
if !keyPattern.MatchString(key) {
return false
}
value := crc32.ChecksumIEEE([]byte(key[:len(key)-6]))
checksum := ""
for value > 0 {
checksum = string(alphabet[value%62]) + checksum
value /= 62
}
return strings.Repeat("A", 6-len(checksum))+checksum == key[len(key)-6:]
}A well formed key can still be unknown, revoked, disabled or expired: only the API knows that.