API reference
Webhooks
Create webhook and pixel endpoints, send a test, read their deliveries, replay failures and rotate signing secrets.
This group manages the endpoints the platform delivers events to. A webhook is a signed JSON envelope posted to your server. A pixel is a URL of yours, templated with macros, fired when an event happens — an ad network's conversion URL, usually. They are the same object with a different kind, so everything below applies to both unless it says otherwise. How deliveries behave is in Webhooks, and the events themselves are listed in Events.
A console surface
Every endpoint on this page is the console's own, under /api/tracking, called with the session token of a signed-in owner or admin. No API key can create, edit, test or replay an endpoint: a URL this platform will call from inside your workspace is a person's decision. The one exception is the event vocabulary, which any valid key may read at /api/v1/webhooks/events.
https://api.buy3.io/api/tracking- Every request may carry an optional
agencyId(a UUID, in the query string or the body). Leave it out when you administer one workspace. - Creating, editing, testing, replaying and rotating a secret need a plan that includes webhooks and pixels. Pausing and deleting never do — a workspace that has changed plan must still be able to stop what it already has.
- How many endpoints a workspace may hold is a plan limit, counting webhooks and pixels together. See pricing.
- Every write here is recorded in the activity log under the kind
webhook.headersandbodyTemplateare recorded as *changed*, never printed: a header is where a receiver's bearer token lives.
What you can do
| Operation | Notes |
|---|---|
| Read the vocabulary | Every event, every macro a pixel may use, a fully filled sample payload and the retry schedule — enough to build an editor against. |
| List and create endpoints | Choose the kind, the URL, the events, and optionally one campaign or one publisher to narrow it to. |
| Update and delete an endpoint | Including pausing it. A paused endpoint is never called, and nothing is queued for it while it is paused. |
| Send a test | Queues one delivery built from your own most recent call, so you test your handler against your own data. |
| Read the delivery log | One entry per event, its attempts inside it, with the request, the response and any error. |
| Replay | One delivery, or every event still failing on an endpoint. |
| Rotate the signing secret | A new secret, shown once. The old one stops verifying at once. |
The endpoint object
| Field | Type | Meaning |
|---|---|---|
id | uuid | The endpoint's id. |
name | string | null | Your name for it. |
kind | string | webhook or pixel. It cannot be changed after creation. |
url | string | Where deliveries go. For a pixel, a template. |
method | string | GET or POST. A webhook is always POST. |
eventPattern | string[] | The events subscribed to, or ["*"] for all of them. |
status | string | active, degraded or paused. See Retries and health. |
campaignId, campaignName | uuid, string | null | The campaign delivery is narrowed to. |
publisherId, publisherName | uuid, string | null | The publisher delivery is narrowed to. |
headers | object | The extra headers sent with each delivery. |
bodyTemplate | string | null | A POST pixel's body template. |
note | string | null | Your own note. |
signed | boolean | Whether deliveries carry a signature. false only for a GET pixel, which has no body to sign. |
secretHint | string | null | The first and last few characters of the signing secret, so you can tell two apart. Never the secret. |
deliveredCount, failedCount | integer | Lifetime counters. |
stats | object | The last 24 hours: { window, attempts, delivered, failed, p50, p95, lastStatus, lastOk, lastAt }. p50 and p95 are milliseconds. A lastStatus of 0 means the request never reached a server. |
createdBy, createdAt, updatedAt | uuid | null, ISO 8601 | Who made it and when it last changed. |
What a receiver gets
A webhook is a POST of this envelope. data is the payload core — the same 26 keys for every call event, with null where the call has no such fact. GET /api/tracking/webhooks/events returns a fully filled sample of it, and a test send is that exact shape plus "test": true.
{
"id": "req_5f0c2a91b7d34e10",
"event": "call.converted",
"createdAt": "2026-09-20T14:06:24.512Z",
"data": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"campaignName": "Medicare — Inbound",
"publisherName": "Northwind Media",
"buyerName": "Acme Health",
"targetName": "Acme Health — Dallas floor",
"callerNumber": "+14155550142",
"dialedNumber": "+18885550100",
"talkSeconds": 184,
"converted": true,
"revenueCents": 4200,
"payoutCents": 2750,
"tags": { "gclid": "Cj0KCQjw" }
}
}| Header | Value |
|---|---|
content-type | application/json |
user-agent | buy3-webhooks/1 |
buy3-event | The event name. |
buy3-request-id | The envelope's id. Stable across retries and replays — de-duplicate on it. |
buy3-attempt | 1 to 4. Anything higher is a replay somebody asked for. |
buy3-signature | t=<unix seconds>,v1=<hex>. Absent on a GET pixel, which has no body to sign. |
| your own | Up to 20 headers of your choosing, macros expanded. |
createdAt is the time of this send, so it — and therefore the signature — differs between attempts. Verify over the raw bytes before parsing: Verifying a webhook signature.Retries and health
- Success is any 2xx. A 3xx is a failure: redirects are never followed, because the address that was vetted is the address that gets called.
- Each attempt times out after 10 seconds.
- Four attempts in all, so three waits: 1 second after the first failure, 5 after the second, 25 after the third. The dispatcher runs on a 15-second tick, so a wait is a floor rather than a promise.
GET /webhooks/eventspublishes the same schedule asretries. - After the fourth failure the machine gives up. Only a replay revives the event, as attempt 5 under the same request id.
- Two consecutive failures mark the endpoint
degradedand write an activity entry with the system as the actor. The next success sets itactiveagain. - A
pausedendpoint is never called, nothing new is queued for it, and anything already queued waits until it is resumed. - The log keeps the first 2,048 bytes of your answer, and for a
POSTpixel the first 8,192 bytes of what was sent.
The delivery log
GET /webhooks/:id/deliveries returns one entry per event, with its attempts inside it. A delivery that failed three times and then landed is one story with a happy ending, and a flat list of attempts prints it as three failures and a success on four unrelated rows.
state | Means |
|---|---|
delivered | Some attempt was accepted. |
queued | None was, and one is still waiting — a retry inside its backoff, or a replay. |
failed | None was, and nothing is waiting. These are what replay failures replays. |
state filter takes is the word an entry and each attempt inside it carry.Pixels, macros and the caller's number
- A pixel's
urlis a template. Every value is percent-encoded as it goes in. - A caller's number never reaches a URL.
{{caller.e164}},{{caller.digits}},{{caller.nanp}}and{{caller.suffix}}are sent as{{caller.hash}}instead, and a tag whose value contains the caller's ten digits becomes the hash too. There is no way to switch this off. Those tokens do expand in aPOSTbody. - The host may not contain a macro. Whoever controls a tag would otherwise choose where this platform sends a request.
- A token the vocabulary does not know is a
400when you save, naming it — not an empty value discovered a month later. A{{tag.…}}key is never unknown. - A missing value expands to nothing. Write
{{{for a literal{{. There is no expression language.
Endpoints
- GET
/api/tracking/webhooks/eventsThe event and macro vocabulary - GET
/api/v1/webhooks/eventsThe event and macro vocabulary, for a key - GET
/api/tracking/webhooksList endpoints - POST
/api/tracking/webhooksCreate an endpoint - GET
/api/tracking/webhooks/:idRead an endpoint - PATCH
/api/tracking/webhooks/:idUpdate an endpoint - DELETE
/api/tracking/webhooks/:idDelete an endpoint - POST
/api/tracking/webhooks/:id/testSend a test delivery - GET
/api/tracking/webhooks/:id/deliveriesList deliveries - POST
/api/tracking/webhook-deliveries/:id/replayReplay one delivery - POST
/api/tracking/webhooks/:id/replay-failuresReplay every failed event - POST
/api/tracking/webhooks/:id/secretRotate the signing secret
The event and macro vocabulary
/api/tracking/webhooks/eventsEverything an editor needs: the ten events with a sentence each, every macro a pixel may use, a sample envelope and payload with all 26 keys filled, the headers a delivery carries and the retry schedule the dispatcher actually runs. Takes no parameters and changes nothing. The same answer is on the REST API at GET /api/v1/webhooks/events, where any valid key opens it and no scope is needed.
Example request
curl "https://api.buy3.io/api/tracking/webhooks/events" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200Abbreviated:
eventslists all ten,macrosthe whole pixel vocabulary. Every token inmacrosexpands to something againstsample.payload— that is held by a test.
{
"events": [
{
"event": "call.completed",
"description": "The call ended. Sent for every call, converted or not, with its final duration and figures."
},
{
"event": "call.converted",
"description": "The call earned its price — by talk time, or because the buyer reported a conversion."
}
],
"macros": [
{
"group": "Call",
"token": "{{call.ref}}",
"description": "The call's public reference.",
"example": "CA-482137"
},
{
"group": "Caller",
"token": "{{caller.e164}}",
"description": "The caller's number.",
"example": "+14155550142",
"inUrl": "hashed"
}
],
"sample": {
"event": "call.converted",
"envelope": {
"id": "req_5f0c2a91b7d34e10",
"event": "call.converted",
"createdAt": "2026-09-20T14:06:24.512Z",
"data": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"campaignName": "Medicare — Inbound",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisherName": "Northwind Media",
"publisherRef": "nw-88231",
"buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"buyerName": "Acme Health",
"targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"targetName": "Acme Health — Dallas floor",
"targetExternalRef": "acme-dal-01",
"callerNumber": "+14155550142",
"callerState": "CA",
"callerCity": "San Francisco",
"callerZip": "94103",
"dialedNumber": "+18885550100",
"startedAt": "2026-09-20T14:03:11.000Z",
"answeredAt": "2026-09-20T14:03:19.000Z",
"endedAt": "2026-09-20T14:06:23.000Z",
"talkSeconds": 184,
"converted": true,
"revenueCents": 4200,
"payoutCents": 2750,
"tags": {
"gclid": "Cj0KCQjw",
"utm_source": "google"
}
}
},
"payload": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"campaignName": "Medicare — Inbound",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisherName": "Northwind Media",
"publisherRef": "nw-88231",
"buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"buyerName": "Acme Health",
"targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"targetName": "Acme Health — Dallas floor",
"targetExternalRef": "acme-dal-01",
"callerNumber": "+14155550142",
"callerState": "CA",
"callerCity": "San Francisco",
"callerZip": "94103",
"dialedNumber": "+18885550100",
"startedAt": "2026-09-20T14:03:11.000Z",
"answeredAt": "2026-09-20T14:03:19.000Z",
"endedAt": "2026-09-20T14:06:23.000Z",
"talkSeconds": 184,
"converted": true,
"revenueCents": 4200,
"payoutCents": 2750,
"tags": {
"gclid": "Cj0KCQjw",
"utm_source": "google"
}
},
"pixel": {
"template": "https://ads.example.com/conversion?click={{tag.gclid}}&value={{call.revenue}}&ref={{call.ref}}&caller={{caller.e164}}",
"expanded": "https://ads.example.com/conversion?click=Cj0KCQjw&value=42.00&ref=CA-482137&caller=9c1185a5c5e9fc54"
}
},
"headers": [
"buy3-event",
"buy3-request-id",
"buy3-attempt",
"buy3-signature"
],
"retries": {
"maxAttempts": 4,
"backoffSeconds": [
1,
5,
25
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | An agencyId that is not a workspace you administer answers the same 404. |
The event and macro vocabulary, for a key
/api/v1/webhooks/eventsThe same answer as the console route above, byte for byte — it is the same handler behind a different door. It is the only route in this group a key can reach, and the only route on the REST API that asks for no scope: the vocabulary is the same for every workspace, so any valid key may read it. Everything else here needs a person, because a URL this platform will call from inside your workspace is a person's decision.
Example request
curl "https://api.buy3.io/api/v1/webhooks/events" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200As above:
events,macros,sample,headersandretries. Abbreviated here.
{
"events": [
{
"event": "call.completed",
"description": "The call ended. Sent for every call, converted or not, with its final duration and figures."
}
],
"macros": [
{
"group": "Call",
"token": "{{call.ref}}",
"description": "The call's public reference.",
"example": "CA-482137"
}
],
"headers": [
"buy3-event",
"buy3-request-id",
"buy3-attempt",
"buy3-signature"
],
"retries": {
"maxAttempts": 4,
"backoffSeconds": [
1,
5,
25
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | missing_key, invalid_key | No Authorization: Bearer header, or a key that is not recognised, revoked, or past its rotation grace. |
| 402 | subscription_inactive, plan_upgrade_required | The workspace's subscription is not active, or its plan does not include call tracking or API access. The REST API has no read-open rule: a lapsed workspace is refused here as well. |
| 403 | ip_not_allowed, workspace_required | The request came from an address that is not on the key's allowlist, or the key belongs to no workspace. |
| 429 | rate_limited | The key is over its per-minute limit. Retry-After says when to come back. |
List endpoints
/api/tracking/webhooksThe workspace's webhooks and pixels, newest first, each with its last 24 hours of stats — so one request answers which receiver is slow as well as which is failing. Changes nothing.
Query parameters
| Name | Type | Description |
|---|---|---|
kindOptional | string | Only endpoints of this kind.webhook or pixel |
statusOptional | string | Only endpoints in this state.active, degraded or paused |
campaignIdOptional | uuid | Only endpoints narrowed to this campaign. |
publisherIdOptional | uuid | Only endpoints narrowed to this publisher. |
qOptional | string | Matches part of the name, the URL or the note, ignoring case.up to 200 characters |
pageOptional | integer | The page to return.from 1, default 1 |
pageSizeOptional | integer | Rows per page. A larger value is clamped, not refused.1–200 · default 25 |
Example request
curl "https://api.buy3.io/api/tracking/webhooks?kind=webhook&status=degraded" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200One page of endpoints in the standard list envelope.
{
"data": [
{
"id": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"name": "Conversion sync",
"kind": "webhook",
"url": "https://hooks.example.com/buy3/calls",
"method": "POST",
"eventPattern": [
"call.completed",
"call.converted"
],
"status": "active",
"campaignId": null,
"campaignName": null,
"publisherId": null,
"publisherName": null,
"headers": {
"x-team": "revops"
},
"bodyTemplate": null,
"note": null,
"signed": true,
"secretHint": "whsec_9Qd2••••••••x71A",
"deliveredCount": 18204,
"failedCount": 37,
"createdBy": "5e4d3c2b-1a09-4887-b6c5-d4e3f2a1b0c9",
"createdAt": "2026-08-02T11:20:41.000Z",
"updatedAt": "2026-09-18T09:02:12.000Z",
"stats": {
"window": "24h",
"attempts": 412,
"delivered": 409,
"failed": 3,
"p50": 118,
"p95": 840,
"lastStatus": 200,
"lastOk": true,
"lastAt": "2026-09-20T14:06:24.981Z"
}
},
{
"id": "9c8b7a65-4321-4098-8765-43210fedcba9",
"name": "Ad network conversion",
"kind": "pixel",
"url": "https://ads.example.com/conversion?click={{tag.gclid}}&value={{call.revenue}}&ref={{call.ref}}",
"method": "GET",
"eventPattern": [
"call.converted"
],
"status": "active",
"campaignId": null,
"campaignName": null,
"publisherId": null,
"publisherName": null,
"headers": {},
"bodyTemplate": null,
"note": null,
"signed": false,
"secretHint": null,
"deliveredCount": 0,
"failedCount": 0,
"createdBy": "5e4d3c2b-1a09-4887-b6c5-d4e3f2a1b0c9",
"createdAt": "2026-09-20T14:00:02.000Z",
"updatedAt": "2026-09-20T14:00:02.000Z",
"stats": {
"window": "24h",
"attempts": 0,
"delivered": 0,
"failed": 0,
"p50": 0,
"p95": 0,
"lastStatus": null,
"lastOk": null,
"lastAt": null
}
}
],
"page": 1,
"pageSize": 25,
"total": 2,
"totalPages": 1
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | kind or status is not one of the allowed words, or an id filter is not a UUID. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | An agencyId that is not a workspace you administer answers the same 404. |
Create an endpoint
/api/tracking/webhooksCreates a webhook or a pixel and, for anything that signs, mints its signing secret — returned in this response and never again. The URL is resolved and vetted before the row exists, and again at every delivery, because the name behind it is yours to change. Recorded in the activity log (webhook, high severity). It sends nothing: the first delivery is the first matching event.
Request body
| Name | Type | Description |
|---|---|---|
nameRequired | string | Your name for the endpoint. It is what the activity log and the delivery log call it.up to 120 characters |
urlRequired | string | Where deliveries go. For a pixel this is a template and may carry macros in the path and the query string — but never in the host.up to 2000 characters · an http(s):// URL with no spaces · must pass the address rules below |
kindOptional | string | What this is. It cannot be changed afterwards — make a new one instead.webhook or pixel · default webhook |
eventPatternRequired | string[] | string | The events to subscribe to, or "*" (or ["*"]) for all of them. A tenant endpoint only ever hears the ten tenant events, so * can never come to mean somebody else's traffic.at most 20 event names · at least one |
methodOptional | string | How the endpoint is called. A webhook must be a POST. A pixel is a GET unless you say otherwise, which is what an ad network's conversion URL is.GET or POST · default POST for a webhook, GET for a pixel |
campaignIdOptional | uuid | Narrow delivery to one campaign of yours. A narrowed endpoint never hears an event that names no campaign. |
publisherIdOptional | uuid | Narrow delivery to one publisher of yours. A narrowed endpoint never hears an event that names no publisher — a call on your own media, for instance. |
headersOptional | object | Extra request headers. Values may use macros. Send null to clear them.at most 20 entries · each value a single line of at most 1000 characters · hop-by-hop names and any name starting buy3- are refused |
bodyTemplateOptional | string | The body of a POST pixel. Without one, a pixel posts the standard envelope. It is escaped for its own content type: JSON by default, percent-encoded for application/x-www-form-urlencoded, verbatim for anything else.up to 10,000 characters · a POST pixel only · must expand to valid JSON unless a non-JSON content-type header is set |
noteOptional | string | A note to yourself. It is searched by q.up to 500 characters |
Example request
curl -X POST "https://api.buy3.io/api/tracking/webhooks" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Conversion sync",
"url": "https://hooks.example.com/buy3/calls",
"eventPattern": ["call.completed", "call.converted"],
"headers": {
"x-team": "revops"
}
}'Responses
- 201The endpoint, plus
signingSecretfor anything that signs andnotes— sentences worth showing a person, such as the fact that a caller macro in a URL is sent hashed.
{
"webhook": {
"id": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"name": "Conversion sync",
"kind": "webhook",
"url": "https://hooks.example.com/buy3/calls",
"method": "POST",
"eventPattern": [
"call.completed",
"call.converted"
],
"status": "active",
"campaignId": null,
"campaignName": null,
"publisherId": null,
"publisherName": null,
"headers": {
"x-team": "revops"
},
"bodyTemplate": null,
"note": null,
"signed": true,
"secretHint": "whsec_9Qd2••••••••x71A",
"deliveredCount": 0,
"failedCount": 0,
"createdBy": "5e4d3c2b-1a09-4887-b6c5-d4e3f2a1b0c9",
"createdAt": "2026-09-20T14:00:02.000Z",
"updatedAt": "2026-09-20T14:00:02.000Z",
"stats": {
"window": "24h",
"attempts": 0,
"delivered": 0,
"failed": 0,
"p50": 0,
"p95": 0,
"lastStatus": null,
"lastOk": null,
"lastAt": null
}
},
"signingSecret": "whsec_9Qd2rN7xKp0aVb3cLm5eTw8hx71A",
"note": "Store this signing secret now — it is shown only once.",
"notes": []
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A required field is missing, or a field has the wrong type or is too long. details maps each field to what was wrong with it. |
| 400 | bad_request | The configuration cannot work. details names the part: method (a webhook must be a POST), url (a macro in the host), macros (naming each unknown token), bodyTemplate (it applies to a POST pixel, or it does not expand to valid JSON), headers (a reserved name, or a value that is too long or spans two lines) or eventPattern (it names no event). |
| 400 | unsafe_url | The URL is not one this platform will call: it is not https in production, it carries credentials in front of the host, or the name resolves to a private, loopback, link-local or metadata address. details.rule says which rule it broke. A pixel is judged on its template expanded against the sample event. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking (details.feature is callTracking), or does not include webhooks and pixels (details.feature is webhooks). It is also the answer when the workspace already holds as many endpoints as its plan allows — then details.feature is maxWebhooks and details carries limit and current beside it. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | The campaignId or publisherId is not in the workspace. An agencyId that is not a workspace you administer answers the same 404. |
Read an endpoint
/api/tracking/webhooks/:idOne endpoint with its last 24 hours of stats. Changes nothing, and never returns the signing secret.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Example request
curl "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200The endpoint.
{
"webhook": {
"id": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"name": "Conversion sync",
"kind": "webhook",
"url": "https://hooks.example.com/buy3/calls",
"method": "POST",
"eventPattern": [
"call.completed",
"call.converted"
],
"status": "active",
"campaignId": null,
"campaignName": null,
"publisherId": null,
"publisherName": null,
"headers": {
"x-team": "revops"
},
"bodyTemplate": null,
"note": null,
"signed": true,
"secretHint": "whsec_9Qd2••••••••x71A",
"deliveredCount": 18204,
"failedCount": 37,
"createdBy": "5e4d3c2b-1a09-4887-b6c5-d4e3f2a1b0c9",
"createdAt": "2026-08-02T11:20:41.000Z",
"updatedAt": "2026-09-18T09:02:12.000Z",
"stats": {
"window": "24h",
"attempts": 412,
"delivered": 409,
"failed": 3,
"p50": 118,
"p95": 840,
"lastStatus": 200,
"lastOk": true,
"lastAt": "2026-09-20T14:06:24.981Z"
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
Update an endpoint
/api/tracking/webhooks/:idChanges what you send. kind is not among the fields — a webhook cannot become a pixel. name and url cannot be cleared; headers: null clears them to {}. The URL and the template are re-vetted only when url, method, headers or bodyTemplate changes. A body of exactly { "status": "paused" } needs no plan feature, so a workspace can always stop an endpoint. Recorded in the activity log (webhook, medium severity) as paused, resumed or updated.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Request body
| Name | Type | Description |
|---|---|---|
nameOptional | string | A new name.up to 120 characters |
urlOptional | string | A new URL, vetted as on create.up to 2000 characters |
eventPatternOptional | string[] | string | Replaces the subscription outright — it is not merged.at most 20 event names · at least one |
statusOptional | string | paused stops deliveries; active resumes them, and also clears degraded. degraded is the dispatcher's word and cannot be set by hand.active or paused |
methodOptional | string | How the endpoint is called. A webhook must be a POST. A pixel is a GET unless you say otherwise, which is what an ad network's conversion URL is.GET or POST · default POST for a webhook, GET for a pixel |
campaignIdOptional | uuid | Narrow delivery to one campaign of yours. A narrowed endpoint never hears an event that names no campaign. |
publisherIdOptional | uuid | Narrow delivery to one publisher of yours. A narrowed endpoint never hears an event that names no publisher — a call on your own media, for instance. |
headersOptional | object | Extra request headers. Values may use macros. Send null to clear them.at most 20 entries · each value a single line of at most 1000 characters · hop-by-hop names and any name starting buy3- are refused |
bodyTemplateOptional | string | The body of a POST pixel. Without one, a pixel posts the standard envelope. It is escaped for its own content type: JSON by default, percent-encoded for application/x-www-form-urlencoded, verbatim for anything else.up to 10,000 characters · a POST pixel only · must expand to valid JSON unless a non-JSON content-type header is set |
noteOptional | string | A note to yourself. It is searched by q.up to 500 characters |
Example request
curl -X PATCH "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "paused"
}'Responses
- 200The endpoint as it now stands, with any
notesthe new configuration raised.
{
"webhook": {
"id": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"name": "Conversion sync",
"kind": "webhook",
"url": "https://hooks.example.com/buy3/calls",
"method": "POST",
"eventPattern": [
"call.completed",
"call.converted"
],
"status": "paused",
"campaignId": null,
"campaignName": null,
"publisherId": null,
"publisherName": null,
"headers": {
"x-team": "revops"
},
"bodyTemplate": null,
"note": null,
"signed": true,
"secretHint": "whsec_9Qd2••••••••x71A",
"deliveredCount": 18204,
"failedCount": 37,
"createdBy": "5e4d3c2b-1a09-4887-b6c5-d4e3f2a1b0c9",
"createdAt": "2026-08-02T11:20:41.000Z",
"updatedAt": "2026-09-18T09:02:12.000Z",
"stats": {
"window": "24h",
"attempts": 412,
"delivered": 409,
"failed": 3,
"p50": 118,
"p95": 840,
"lastStatus": 200,
"lastOk": true,
"lastAt": "2026-09-20T14:06:24.981Z"
}
},
"notes": []
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A field has the wrong type or is out of range. |
| 400 | bad_request | The body is empty ("Nothing to update"), or name or url was sent as null. The configuration cannot work. details names the part: method (a webhook must be a POST), url (a macro in the host), macros (naming each unknown token), bodyTemplate (it applies to a POST pixel, or it does not expand to valid JSON), headers (a reserved name, or a value that is too long or spans two lines) or eventPattern (it names no event). |
| 400 | unsafe_url | The URL is not one this platform will call: it is not https in production, it carries credentials in front of the host, or the name resolves to a private, loopback, link-local or metadata address. details.rule says which rule it broke. A pixel is judged on its template expanded against the sample event. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking (details.feature is callTracking), or does not include webhooks and pixels (details.feature is webhooks). It is also the answer when the workspace already holds as many endpoints as its plan allows — then details.feature is maxWebhooks and details carries limit and current beside it. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
Delete an endpoint
/api/tracking/webhooks/:idDeletes the endpoint and its whole delivery log with it. There is no undo and no export afterwards, so read the deliveries you need first. Never gated on the plan. Recorded in the activity log (webhook, medium severity) with the lifetime counters kept in the entry.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Example request
curl -X DELETE "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 204Deleted. No body.
Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
Send a test delivery
/api/tracking/webhooks/:id/testQueues one delivery built from the workspace's most recent real call — inside the endpoint's own campaign or publisher narrowing — falling back to sample data when the workspace has no calls yet. It is built by the same function every live event is built by, so apart from "test": true it is, key for key, what that call's own event carried. Its request id begins test_. Nothing is sent while you wait: the answer is 202 and the delivery log has the result within about 15 seconds. Recorded in the activity log (webhook, low severity).
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Request body
| Name | Type | Description |
|---|---|---|
eventOptional | string | Which event to imitate.one of the ten event names · default: the endpoint's first subscribed event, else call.completed |
Example request
curl -X POST "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071/test" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "call.converted"
}'Responses
- 202
sourcesays whether the payload came from one of your calls or from the fixture. The delivery is returned in full, queued: no status code yet.
{
"delivery": {
"id": "9928640",
"requestId": "test_1f0a9b8c-7d6e-4f50-a1b2-c3d4e5f60718",
"event": "call.converted",
"endpointId": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"endpoint": "https://hooks.example.com/buy3/calls",
"method": "POST",
"code": null,
"ms": 0,
"attempt": 0,
"ok": false,
"state": "queued",
"error": null,
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"at": null,
"url": "https://hooks.example.com/buy3/calls",
"payload": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"campaignName": "Medicare — Inbound",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisherName": "Northwind Media",
"publisherRef": "nw-88231",
"buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"buyerName": "Acme Health",
"targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"targetName": "Acme Health — Dallas floor",
"targetExternalRef": "acme-dal-01",
"callerNumber": "+14155550142",
"callerState": "CA",
"callerCity": "San Francisco",
"callerZip": "94103",
"dialedNumber": "+18885550100",
"startedAt": "2026-09-20T14:03:11.000Z",
"answeredAt": "2026-09-20T14:03:19.000Z",
"endedAt": "2026-09-20T14:06:23.000Z",
"talkSeconds": 184,
"converted": true,
"revenueCents": 4200,
"payoutCents": 2750,
"tags": {
"gclid": "Cj0KCQjw",
"utm_source": "google"
},
"test": true
},
"requestBody": null,
"responseBody": null,
"retryAfter": null
},
"source": "call",
"note": "Queued a test call.converted built from your most recent call (CA-482137). It is sent within 15 seconds; the delivery log shows the request and the response."
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | event is not one of the ten event names. |
| 409 | conflict | The endpoint is paused — a paused endpoint is never called, so the test would sit queued for ever. Or five test deliveries are already waiting on it. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking (details.feature is callTracking), or does not include webhooks and pixels (details.feature is webhooks). It is also the answer when the workspace already holds as many endpoints as its plan allows — then details.feature is maxWebhooks and details carries limit and current beside it. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
List deliveries
/api/tracking/webhooks/:id/deliveriesOne entry per event, newest event first, with every attempt inside it and the payload printed once. Each attempt carries the address actually called — a pixel's template expanded — and the first 2,048 bytes of the answer. Changes nothing.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Query parameters
| Name | Type | Description |
|---|---|---|
stateOptional | string | The state of the event, not of one attempt.delivered, queued or failed |
eventOptional | string | Only this event name.up to 100 characters |
callIdOptional | uuid | Only deliveries about this call. This is how you answer "did our CRM hear about this call". |
pageOptional | integer | The page to return.from 1, default 1 |
pageSizeOptional | integer | Events per page. A larger value is clamped, not refused.1–200 · default 25 |
Example request
curl "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071/deliveries?state=failed&pageSize=50" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200
attemptscounts the attempts actually made, so a queued retry is not counted yet.lastCodeis the most recent HTTP status;0means the request never reached a server.
{
"data": [
{
"requestId": "req_4c1b0a98d7e64f22",
"event": "call.completed",
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"state": "delivered",
"attempts": 2,
"firstAt": "2026-09-20T14:06:24.102Z",
"lastAt": "2026-09-20T14:06:25.640Z",
"lastCode": 200,
"payload": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"campaignName": "Medicare — Inbound",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisherName": "Northwind Media",
"publisherRef": "nw-88231",
"buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"buyerName": "Acme Health",
"targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"targetName": "Acme Health — Dallas floor",
"targetExternalRef": "acme-dal-01",
"callerNumber": "+14155550142",
"callerState": "CA",
"callerCity": "San Francisco",
"callerZip": "94103",
"dialedNumber": "+18885550100",
"startedAt": "2026-09-20T14:03:11.000Z",
"answeredAt": "2026-09-20T14:03:19.000Z",
"endedAt": "2026-09-20T14:06:23.000Z",
"talkSeconds": 184,
"converted": true,
"revenueCents": 4200,
"payoutCents": 2750,
"tags": {
"gclid": "Cj0KCQjw",
"utm_source": "google"
}
},
"deliveries": [
{
"id": "9928470",
"requestId": "req_4c1b0a98d7e64f22",
"event": "call.completed",
"endpointId": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"endpoint": "https://hooks.example.com/buy3/calls",
"method": "POST",
"code": 502,
"ms": 240,
"attempt": 1,
"ok": false,
"state": "failed",
"error": "Bad Gateway",
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"at": "2026-09-20T14:06:24.102Z",
"url": "https://hooks.example.com/buy3/calls",
"requestBody": null,
"responseBody": "upstream connect error",
"retryAfter": null
},
{
"id": "9928471",
"requestId": "req_4c1b0a98d7e64f22",
"event": "call.completed",
"endpointId": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"endpoint": "https://hooks.example.com/buy3/calls",
"method": "POST",
"code": 200,
"ms": 118,
"attempt": 2,
"ok": true,
"state": "delivered",
"error": null,
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"at": "2026-09-20T14:06:25.640Z",
"url": "https://hooks.example.com/buy3/calls",
"requestBody": null,
"responseBody": "{\"ok\":true}",
"retryAfter": null
}
]
}
],
"page": 1,
"pageSize": 25,
"total": 1,
"totalPages": 1
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | state is not one of the three words, or callId is not a UUID. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
Replay one delivery
/api/tracking/webhook-deliveries/:id/replayRe-queues that event under the same request id, with the attempt number carried forward — so a fifth attempt in the log is a person overriding the give-up rule, which is exactly what it was. Two clicks are one replay, because your receiver de-duplicates on the request id. Answers 202; the send follows within about 15 seconds. Recorded in the activity log (webhook, medium severity).
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | string | The delivery's id, from the delivery log. It is a large integer sent as a string, not a UUID.1–18 digits |
Example request
curl -X POST "https://api.buy3.io/api/tracking/webhook-deliveries/9928470/replay" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 202The re-queued attempt.
{
"delivery": {
"id": "9928702",
"requestId": "req_5f0c2a91b7d34e10",
"event": "call.converted",
"endpointId": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"endpoint": "https://hooks.example.com/buy3/calls",
"method": "POST",
"code": null,
"ms": 0,
"attempt": 5,
"ok": false,
"state": "queued",
"error": null,
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"at": null,
"url": "https://hooks.example.com/buy3/calls",
"payload": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"campaignName": "Medicare — Inbound",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisherName": "Northwind Media",
"publisherRef": "nw-88231",
"buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"buyerName": "Acme Health",
"targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"targetName": "Acme Health — Dallas floor",
"targetExternalRef": "acme-dal-01",
"callerNumber": "+14155550142",
"callerState": "CA",
"callerCity": "San Francisco",
"callerZip": "94103",
"dialedNumber": "+18885550100",
"startedAt": "2026-09-20T14:03:11.000Z",
"answeredAt": "2026-09-20T14:03:19.000Z",
"endedAt": "2026-09-20T14:06:23.000Z",
"talkSeconds": 184,
"converted": true,
"revenueCents": 4200,
"payoutCents": 2750,
"tags": {
"gclid": "Cj0KCQjw",
"utm_source": "google"
}
},
"requestBody": null,
"responseBody": null,
"retryAfter": null
},
"note": "Queued. It is sent within 15 seconds and the log records the result."
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such delivery in the workspace. Another workspace's delivery, and an id that is not 1–18 digits, answer exactly as a missing one does. |
| 409 | conflict | The delivery is still queued — there is nothing to replay yet — or its endpoint is paused. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking (details.feature is callTracking), or does not include webhooks and pixels (details.feature is webhooks). It is also the answer when the workspace already holds as many endpoints as its plan allows — then details.feature is maxWebhooks and details carries limit and current beside it. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | An agencyId that is not a workspace you administer answers the same 404. |
Replay every failed event
/api/tracking/webhooks/:id/replay-failuresOne replay per failed event — its latest attempt — skipping anything that has since succeeded or is already queued, so no receiver is sent an event it already took. It looks at the endpoint's newest 1,000 stamped attempts. This is the button on the banner after an outage of yours. Answers 202. Recorded in the activity log (webhook, medium severity).
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Request body
| Name | Type | Description |
|---|---|---|
limitOptional | integer | The most events to re-queue in this call. Run it again for more.1–500 · default 200 |
Example request
curl -X POST "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071/replay-failures" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"limit": 100
}'Responses
- 202How many events were queued, in order.
- 202 (nothing to do)Nothing was failing.
{
"queued": 37,
"note": "Queued in order. Watch the delivery log for the results."
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | limit is not an integer between 1 and 500. |
| 409 | conflict | The endpoint is paused. Resume it first — a paused endpoint is never called. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking (details.feature is callTracking), or does not include webhooks and pixels (details.feature is webhooks). It is also the answer when the workspace already holds as many endpoints as its plan allows — then details.feature is maxWebhooks and details carries limit and current beside it. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
Rotate the signing secret
/api/tracking/webhooks/:id/secretMints a new signing secret and returns it once. A stored secret is never readable, so the way to recover a lost one is a new one. Takes no body. Recorded in the activity log (webhook, high severity) as the fact that the secret changed — never its value.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The endpoint's id. |
Example request
curl -X POST "https://api.buy3.io/api/tracking/webhooks/7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071/secret" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200The endpoint with its new
secretHint, and the secret itself for the only time.
{
"webhook": {
"id": "7a1b2c3d-4e5f-4061-8a7b-2c3d4e5f6071",
"name": "Conversion sync",
"kind": "webhook",
"url": "https://hooks.example.com/buy3/calls",
"method": "POST",
"eventPattern": [
"call.completed",
"call.converted"
],
"status": "active",
"campaignId": null,
"campaignName": null,
"publisherId": null,
"publisherName": null,
"headers": {
"x-team": "revops"
},
"bodyTemplate": null,
"note": null,
"signed": true,
"secretHint": "whsec_2Lp8••••••••q4Zk",
"deliveredCount": 18204,
"failedCount": 37,
"createdBy": "5e4d3c2b-1a09-4887-b6c5-d4e3f2a1b0c9",
"createdAt": "2026-08-02T11:20:41.000Z",
"updatedAt": "2026-09-18T09:02:12.000Z",
"stats": {
"window": "24h",
"attempts": 412,
"delivered": 409,
"failed": 3,
"p50": 118,
"p95": 840,
"lastStatus": 200,
"lastOk": true,
"lastAt": "2026-09-20T14:06:24.981Z"
}
},
"signingSecret": "whsec_2Lp8vR1yNc6fJt9wEs0bq4Zk",
"note": "Store this signing secret now — it is shown only once. Deliveries are signed with it from the next one; update your receiver before it rejects them."
}Errors
| Status | Code | When |
|---|---|---|
| 409 | conflict | The endpoint is a GET pixel. It has no body to sign, so it has no secret to rotate. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active. Reads stay open to a lapsed workspace; writes do not. details carries status, planId and agencyId. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking (details.feature is callTracking), or does not include webhooks and pixels (details.feature is webhooks). It is also the answer when the workspace already holds as many endpoints as its plan allows — then details.feature is maxWebhooks and details carries limit and current beside it. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | No such endpoint in the workspace. Another workspace's endpoint and an id that is not a UUID answer exactly as a missing one does. An agencyId that is not a workspace you administer answers the same 404. |
