Keys and security

Publisher routing keys end to end: what a key is, how it is issued, rotated and revoked, and the four controls on it — protocol, IP allowlist, rate limits and request signing.

After this page you can issue a publisher a routing key, lock it to their addresses, set how fast they may ping, require signed requests, rotate any of it without taking their traffic down, and find every use of the key afterwards.

A publisher who pings you authenticates with a routing key. A key belongs to one lane — one publisher on one campaign — so the key alone tells the platform who is asking and about which campaign, and revoking it cuts off exactly that and nothing else.

The key

  • A key begins b3k_live_ and is sent as a bearer token: Authorization: Bearer b3k_live_….
  • The full key is shown once, in the response that issued it. Only a SHA-256 hash authenticates it, so nothing can authenticate against a stored copy.
  • An encrypted copy is kept so the key can be read back once more — see Reading a key back. It is never part of any list, detail or portal response; every one of those carries the 13-character prefix, such as b3k_live_9f2a, and nothing else.
  • There is one live key per lane. Issuing a second is refused until the first is revoked or rotated.
  • A key carries a label for your own use. It is shown to the publisher too, so write the lane, not a reminder to yourself.
StateWhat it means
liveIssued and in use.
rotatingReplaced, and still accepted until its grace window closes. This is a normal state, not a broken one.
revokedFinished. Every request answers 401 invalid_key.
disabledSwitched off without being replaced.
A rotated key is stamped revoked the moment its replacement exists — the database holds one live key per lane — and keeps working until its grace passes. A console that showed that as "revoked" would have every publisher raising a ticket mid-rotation.

Protocol and environment

protocolWhich surface the key may use
rtb_v1The auction: ping and post at /api/rtb/v1.
legacyThe older availability surface at /api/routing.
bothEither.
A key presented to the wrong surface answers 403 wrong_protocol (reject code 4005) rather than a 401 that looks like a bad secret. A key minted with a lane on a tracking campaign already speaks rtb_v1.

environment is production, test or sandbox. A test key prices and reserves for real and answers with a destination that cannot be dialled, adding warning 220; a sandbox key prices synthetically and adds 222. Both are real reservations, so a publisher should release a test bid rather than leave it to expire.

Issuing, rotating and revoking

ActionWhat happensThe old key
IssueOpening a lane mints the key in the same request. The plaintext comes back once.
RotateRevoke and reissue as one move. The replacement inherits the whole configuration.Keeps working for the grace window you choose — 60 minutes by default, up to a week.
RevokeImmediate. Use it when a key is compromised and every second it lives is a cost.Stops working at once. There is no grace.
Self-rotateThe publisher replaces their own key from their portal.Keeps working for 24 hours. The window is fixed, not theirs to choose.
Rotate with a day of grace
curl -X POST "https://api.buy3.io/api/publishers/keys/b7c8d9e0-1a2b-4c3d-8e4f-5a6b7c8d9e0f/rotate" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "graceMinutes": 1440 }'
201
{
  "key": {
    "id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f70",
    "prefix": "b3k_live_4c71",
    "status": "live",
    "protocol": "rtb_v1",
    "environment": "production",
    "issuedAt": "2026-09-20T14:10:00.000Z",
    "signature": { "required": false, "hasSecret": false, "secretLast4": null },
    "ipAllowlist": ["203.0.113.0/24", "198.51.100.17"],
    "limits": { "perMinute": 240, "perHour": 5000, "perDay": 50000, "maxLiveBidsPerCaller": 2 }
  },
  "plaintextKey": "b3k_live_4c71e8a29d3b6f7015c4a8e2d9b0f3a6",
  "previousKeyId": "b7c8d9e0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
  "graceUntil": "2026-09-21T14:10:00.000Z",
  "note": "Store this key now — it is shown only once. The previous key keeps working until 2026-09-21T14:10:00.000Z."
}

A rotation carries the lane's configuration forward — label, protocol, environment, signature policy and secret, IP allowlist, rate limits, self-rotation and the single-step ping URL. That matters: a rotation that quietly reset the protocol would take a publisher off the auction the moment their grace ran out, which is the exact outage the grace window exists to prevent. The single-step URL moves to the replacement too, so a publisher who only uses that URL notices nothing.

Letting a publisher rotate their own

Two independent switches, and both must be on: the person holds Can rotate keys on their membership, and the key allows self-rotation. The first is the publisher's account owner deciding which teammate may do it; the second is you deciding which lanes are safe to let a partner cut over unsupervised. Neither implies the other.

Reading a key back

A routing key is a shared integration secret, not a password: it lives in a partner's dialler and has to be re-sent by hand when they change platform or put a new operator on the account. Making that a rotation means credentials get rotated for clerical reasons, and every rotation is a window in which live traffic can fail. So a key can be read back, from one endpoint, at the price of a high-severity entry on the activity log naming the reader — every single time.

GET /api/publishers/keys/{keyId}/secret answers with the key itself. A key issued before keys were retained has no stored copy and answers 409 saying so, rather than a 404 that would send you looking for the wrong thing.

IP allowlists

An IP allowlist restricts a key to the addresses you list. A request from anywhere else is refused with 403 ip_not_allowed before the body is looked at, and the refusal is a row in the ping ledger like everything else.

  • An empty list admits everybody. That is what a key is until you say otherwise.
  • An IPv4 entry may carry a prefix length — 203.0.113.0/24 — or be a bare address, which is read as a single host.
  • An IPv6 entry is matched exactly, as a string. There is no prefix matching for IPv6, so list the addresses.
  • A proxy in front of the platform reports IPv4 as an IPv6-mapped address; that is normalised before matching, so 203.0.113.0/24 means what you expect.
  • Up to 64 entries. An entry that does not parse is refused when you save it and named in the error — an allowlist that silently matches nobody is an outage nobody can see.

Rate limits

Pings are limited per key in three fixed windows — the clock minute, the clock hour and the day — plus a cap on how many live bids one caller may hold on that key. The defaults are 120 a minute, 5,000 an hour, 50,000 a day and 4 live bids per caller, and each is editable per key.

Headers on a scored ping
RateLimit-Policy: "minute";q=120;w=60, "hour";q=5000;w=3600, "day";q=50000;w=86400
RateLimit: "minute";r=57
  • Every ping counts, including the ones that are refused. Posts and releases are not counted.
  • Over any window the answer is 429 with reject code 1024, a Retry-After header and the seconds until that window rolls over.
  • Over the live-bid cap the ping is a no-bid with 1026. The publisher posts or releases one first.
  • The publisher reads their own limits from whoami; they never have to ask you for them.
Change the controls on a key
curl -X PATCH "https://api.buy3.io/api/publishers/keys/b7c8d9e0-1a2b-4c3d-8e4f-5a6b7c8d9e0f" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Northwind — Acme Health inbound",
    "protocol": "rtb_v1",
    "allowCidrs": ["203.0.113.0/24", "198.51.100.17"],
    "rateLimitPerMinute": 240,
    "maxLiveBidsPerCaller": 2,
    "allowSelfRotate": true
  }'

Signed requests

A bearer key proves who is asking. It does not prove the request was not altered on the way, or captured and sent again — and a captured ping replayed inside its expiry is a second reservation on the same person. A key can therefore require signed requests.

HeaderValue
X-Buy3-TimestampThe current Unix time, in seconds.
X-Buy3-NonceA random value, used once ever. At least 16 random bytes.
X-Buy3-Signaturet=<timestamp>,v1=<hex digest>. Only v1 is read; any other field is ignored, and any other scheme is refused.
The string to sign
POST   {timestamp}.{nonce}.{raw request body}
GET    {timestamp}.{nonce}.{raw query string, without the leading ?}

v1 = hex( HMAC-SHA256( key = signing secret, message = the string above ) )
  • The raw bytes are what is verified, before the body is parsed. A client that re-serialises after signing fails with 4001.
  • A GET is signed too, over its query string exactly as sent. There is no unsigned door onto a key that requires signatures.
  • The timestamp must be within 300 seconds of the platform's clock, either way, or the answer is 4002.
  • A nonce is remembered only after the signature checks out, so nobody without the secret can burn a sender's nonce space.
  • Worked examples in three languages are on Ping and post.
CodeerrorWhat went wrong
4000signature_missingOne of the three headers, or the v1 field, is absent.
4001signature_invalidThe digest does not match: the bytes signed are not the bytes sent, or the secret is an old one.
4002signature_staleThe timestamp is more than 300 seconds out. Check the sending server's clock.
4003nonce_replayThat nonce has been seen before.
All four answer 401. They are refusals of the request, not of the key, and each is a row in the ping ledger with the reason named.

Signing secrets

The signing secret is a second credential on the same key. It begins b3s_, is stored encrypted, and is shown once — when it is issued and again when it is rotated. Only its last four characters are ever readable afterwards, on the key and in the publisher's own portal, which is enough for both sides to agree they hold the same one.

Mint a secret without requiring it yet
curl -X POST "https://api.buy3.io/api/publishers/keys/b7c8d9e0-1a2b-4c3d-8e4f-5a6b7c8d9e0f/signing-secret/rotate" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "enforce": false }'
200
{
  "key": {
    "id": "b7c8d9e0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
    "prefix": "b3k_live_9f2a",
    "signature": { "required": false, "hasSecret": true, "secretLast4": "9c14" }
  },
  "signingSecret": "b3s_7d1e0a4c8b62f95301ae7c4d2b89c14",
  "effective": "immediately",
  "previousSecretValid": false,
  "note": "Store this now — it is shown only once. Signing is not required yet: deploy the secret, then switch requireSignature on."
}

Rotate without an outage instead:

  1. Turn require signature off on the key. Signed requests are still accepted; they are simply no longer demanded.
  2. Rotate the secret with enforce: false, so minting it does not switch enforcement back on.
  3. Send the new secret to the publisher and wait for them to deploy it.
  4. Turn require signature back on. No request is ever refused for carrying yesterday's secret.
  • Requiring signatures is refused until the key has a secret. A key that demands a signature it cannot verify refuses everything its publisher can possibly send, so the combination is rejected when you try to save it, with the field named.
  • Rotating the key is different from rotating the secret. A key rotation creates a new row, and the old key keeps its own secret for its whole grace window — which is why the two credentials are deployed as a pair.
  • A publisher rotating their own key from the portal gets a fresh secret with it, whenever the key signs at all.

The single-step URL token

The single-step ping URL carries its own credential in the path, separate from the bearer key on purpose: a URL ends up in proxy logs, screenshots and browser histories, and the answer to one leaking must not be rotating the key underneath a working integration.

  • It is off until you switch it on, and switching it on mints the token.
  • Switching it off forgets the token, so turning it back on gives a new URL. A token that has been in a URL has been in places nobody can enumerate.
  • Replacing the URL is its own action and takes effect immediately — there is no grace, because a leaked URL is one somebody else already has.
  • Treat the token as opaque: it is a string of letters, digits, _ and -, and its length is not part of the contract.

Let the publisher check for themselves

GET /api/rtb/v1/whoami answers with everything a publisher would otherwise ask you for: whether their key is usable, the campaign's rules, their limits, their allowlist, whether they must sign and the exact string to sign for each method. Point them at it before they write a line of code.

curl
curl "https://api.buy3.io/api/rtb/v1/whoami" \
  -H "Authorization: Bearer $BUY3_PUBLISHER_KEY"

A recognised but blocked key still answers 200, with ok: false and the problem named — wrong_protocol, publisher_not_live or campaign_not_live. Only a missing or unknown key is an error. The endpoint checks the key alone: it does not apply the IP allowlist or the signature rule, so it answers from anywhere.

What is recorded

WhereWhat is kept
Ping ledgerEvery request, including the refused ones: the endpoint, the method, the request as it was sent, the source address, the user agent, the decision, the reason, the status, the time it took and the answer.
Activity logEvery change to a key, with the old value and the new: protocol, signing, allowlist, limits, self-rotation, the single-step URL, each rotation and each revocation.
The key rowHow many times it has been used, and when it was last used.
  • Credentials are never in either record. A change to the key, the signing secret or the URL token is logged as having happened, never as a value.
  • The single-step door logs the door's name, not the token in the path.
  • Stored request and response bodies are scrubbed before they are shown — a field that looks like a credential, and the per-call postback key on a claimed response, come back blanked.
  • A caller's number is masked unless the campaign discloses caller ID, in the column and inside the stored body.

A worked example

BlueSky Legal are about to go live and want their integration locked down.

  1. Issue — opening their lane mints the key. You send them the plaintext once, with a link to Ping and post.
  2. Allowlist — they give you two outbound ranges. You save 203.0.113.0/24 and 198.51.100.17. Anything else now answers 403 ip_not_allowed before it reaches the auction.
  3. Limits — their own forecast is four pings a second at peak, so you raise the minute limit and drop live bids per caller to 2.
  4. Signing — you rotate a signing secret with enforce: false, send it, and wait. They deploy, test against whoami, and confirm. You switch require signature on. Nothing was ever refused for a secret nobody had yet.
  5. Six weeks later — a laptop with the key on it is lost. You rotate the key with a grace of one day, send the replacement the same afternoon, and their traffic never stops. Had the key been published rather than misplaced, you would revoke it instead and accept the gap.

Next steps