{"openapi":"3.1.0","info":{"title":"BorakDesk API","version":"1.0.0","description":"The BorakDesk public REST API.\n\n**Stability.** `v1` is not \"the version before v2\" — it is a promise that anything under `/v1/` keeps working. It may gain fields and endpoints forever; it will not remove a field, rename one, or change a type.\n\n**Authentication.** Send your key as `Authorization: Bearer brd_live_…`. Basic auth with a blank username and the key as the password also works, for tools whose connectors do not offer bearer tokens.\n\n**Scoping.** A key is issued against one Brand and can only see that Brand's data. A key issued account-wide sees the whole account. Each key also carries scopes, so a key that creates contacts cannot be repurposed to delete them.\n\n**Regions.** A key reaches exactly one region, because it lives in that region's database alongside the data it can read. An account spanning regions uses one key per region."},"servers":[{"url":"https://api.borakdesk.com/api","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Your API key from Admin Center → API Keys. Not your login password."}},"schemas":{"Error":{"type":"object","required":["error"],"description":"Structured on purpose. A client cannot branch on a sentence, and it must be able to tell \"rate limited, retry\" from \"that field is invalid, do not retry\" — and to keep telling them apart after we reword the sentence.","properties":{"error":{"type":"object","required":["type","code","message"],"properties":{"type":{"type":"string","description":"The coarse class to branch on.","enum":["authentication_error","permission_error","invalid_request","not_found","conflict","rate_limit","api_error"]},"code":{"type":"string","description":"The specific reason, e.g. `contact_exists`."},"message":{"type":"string","description":"For a human reading a log. May change at any time — never match on it."},"param":{"type":"string","description":"The offending field, when one field is at fault."},"retryAfter":{"type":"integer","description":"Seconds. Present on rate_limit."}}}}},"Contact":{"type":"object","required":["id","object"],"properties":{"id":{"type":"string"},"object":{"type":"string","enum":["contact"]},"email":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"company":{"type":"string","nullable":true},"jobTitle":{"type":"string","nullable":true},"lifecycleStage":{"type":"string","nullable":true},"leadStatus":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true}}},"Campaign":{"type":"object","required":["id","object"],"properties":{"id":{"type":"string"},"object":{"type":"string","enum":["campaign"]},"name":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"type":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true},"previewText":{"type":"string","nullable":true},"fromName":{"type":"string","nullable":true},"fromEmail":{"type":"string","nullable":true},"replyTo":{"type":"string","nullable":true},"templateId":{"type":"string","nullable":true},"audienceList":{"nullable":true,"description":"Free-form JSON."},"segmentRules":{"nullable":true,"description":"Free-form JSON."},"scheduledAt":{"type":"string","format":"date-time","nullable":true},"sentAt":{"type":"string","format":"date-time","nullable":true},"stats":{"type":"object","required":["recipients","delivered","opened","clicked","replied","bounced","unsubscribed"],"properties":{"recipients":{"type":"integer"},"delivered":{"type":"integer"},"opened":{"type":"integer"},"clicked":{"type":"integer"},"replied":{"type":"integer"},"bounced":{"type":"integer"},"unsubscribed":{"type":"integer"}}},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true},"htmlContent":{"type":"string","nullable":true},"textContent":{"type":"string","nullable":true}}},"Flow":{"type":"object","required":["id","object"],"properties":{"id":{"type":"string"},"object":{"type":"string","enum":["flow"]},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"flowType":{"type":"string","nullable":true},"stats":{"type":"object","required":["enrolled","active","completed","goalReached"],"properties":{"enrolled":{"type":"integer"},"active":{"type":"integer"},"completed":{"type":"integer"},"goalReached":{"type":"integer"}}},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true}}},"ContactList":{"type":"object","required":["object","data","has_more","next_cursor"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"has_more":{"type":"boolean","description":"True when another page follows."},"next_cursor":{"type":"string","nullable":true,"description":"Pass as `starting_after` for the next page. Null on the last page."}}},"CampaignList":{"type":"object","required":["object","data","has_more","next_cursor"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}},"has_more":{"type":"boolean","description":"True when another page follows."},"next_cursor":{"type":"string","nullable":true,"description":"Pass as `starting_after` for the next page. Null on the last page."}}},"FlowList":{"type":"object","required":["object","data","has_more","next_cursor"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Flow"}},"has_more":{"type":"boolean","description":"True when another page follows."},"next_cursor":{"type":"string","nullable":true,"description":"Pass as `starting_after` for the next page. Null on the last page."}}}}},"paths":{"/v1/account":{"get":{"summary":"Who this key belongs to","security":[{"bearerAuth":[]}],"description":"Requires a valid key and **no scope**, so it is safe to use as a connection test. Returns the account and Brand the key is bound to, the region it reaches, and the scopes it carries — the last being the answer to most \"why am I getting a 403\" questions.","responses":{"200":{"description":"The account this key belongs to.","content":{"application/json":{"schema":{"type":"object","required":["object","region","scopes"],"properties":{"object":{"type":"string","enum":["account"]},"name":{"type":"string","nullable":true},"brand":{"type":"object","nullable":true,"description":"Null for an account-wide key — a deliberate choice, not a missing value.","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"region":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/contacts":{"get":{"summary":"List contacts","security":[{"bearerAuth":[]}],"x-scope":"contacts.read","description":"Requires the `contacts.read` scope.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"How many to return. Above 200 is an error, not a silent clamp."},{"name":"starting_after","in":"query","required":false,"schema":{"type":"string"},"description":"The `next_cursor` from the previous page. Cursor, not offset — an offset page silently skips rows when data is written while you iterate."},{"name":"order","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Oldest first (`asc`, the default) or newest first (`desc`). **Use `desc` for anything that polls.** A poller reads the first page and deduplicates by id, so with the default order a newly created row lands at the end of the last page and the poll stops seeing new records once you have more than one page — without erroring."},{"name":"email","in":"query","schema":{"type":"string"},"description":"Exact match, case-insensitive."},{"name":"lifecycleStage","in":"query","schema":{"type":"string"}},{"name":"updatedSince","in":"query","schema":{"type":"string","format":"date-time"},"description":"Everything changed since you last polled. The filter a sync integration actually needs — without it, every poll re-reads the whole list."}],"responses":{"200":{"description":"A page of contacts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactList"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a contact","security":[{"bearerAuth":[]}],"x-scope":"contacts.write","description":"Needs at least an email or a phone number.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Writable fields only: email, firstName, lastName, phone, company, jobTitle, lifecycleStage, leadStatus, city, state, country, tags. An unknown field is rejected, not ignored — silently dropping one is how an integration \"works\" while never saving what its author thought it was saving.","properties":{"email":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"company":{"type":"string","nullable":true},"jobTitle":{"type":"string","nullable":true},"lifecycleStage":{"type":"string","nullable":true},"leadStatus":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"The created contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Missing an identifier, or an unknown field was sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A contact with that email already exists. The message names its id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/contacts/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The contact's id."}],"get":{"summary":"Retrieve a contact","security":[{"bearerAuth":[]}],"x-scope":"contacts.read","description":"Requires the `contacts.read` scope.","responses":{"200":{"description":"The contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a contact","security":[{"bearerAuth":[]}],"x-scope":"contacts.write","description":"Requires the `contacts.write` scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Writable fields only: email, firstName, lastName, phone, company, jobTitle, lifecycleStage, leadStatus, city, state, country, tags. An unknown field is rejected, not ignored — silently dropping one is how an integration \"works\" while never saving what its author thought it was saving.","properties":{"email":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"company":{"type":"string","nullable":true},"jobTitle":{"type":"string","nullable":true},"lifecycleStage":{"type":"string","nullable":true},"leadStatus":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The updated contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete a contact","security":[{"bearerAuth":[]}],"x-scope":"contacts.delete","description":"Recoverable from Admin Center → Deleted Items within your retention window. A deletion through an API key is the one most likely to be a mistake nobody notices, so it is as recoverable as a deletion through a screen.","responses":{"200":{"description":"Deleted."},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/campaigns":{"get":{"summary":"List campaigns","security":[{"bearerAuth":[]}],"x-scope":"campaigns.read","description":"Email bodies are omitted from lists — a page of 200 campaigns each carrying a full HTML body is a multi-megabyte response nobody asked for. Retrieve one to get its `htmlContent` and `textContent`.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"How many to return. Above 200 is an error, not a silent clamp."},{"name":"starting_after","in":"query","required":false,"schema":{"type":"string"},"description":"The `next_cursor` from the previous page. Cursor, not offset — an offset page silently skips rows when data is written while you iterate."},{"name":"order","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Oldest first (`asc`, the default) or newest first (`desc`). **Use `desc` for anything that polls.** A poller reads the first page and deduplicates by id, so with the default order a newly created row lands at the end of the last page and the poll stops seeing new records once you have more than one page — without erroring."},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string"}},{"name":"updatedSince","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"A page of campaigns, without email bodies.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignList"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a draft campaign","security":[{"bearerAuth":[]}],"x-scope":"campaigns.write","description":"Requires the `campaigns.write` scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Writable fields only: name, type, subject, previewText, fromName, fromEmail, replyTo, templateId, htmlContent, textContent, audienceList, segmentRules, scheduledAt. An unknown field is rejected, not ignored — silently dropping one is how an integration \"works\" while never saving what its author thought it was saving.","properties":{"name":{"type":"string","nullable":true},"type":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true},"previewText":{"type":"string","nullable":true},"fromName":{"type":"string","nullable":true},"fromEmail":{"type":"string","nullable":true},"replyTo":{"type":"string","nullable":true},"templateId":{"type":"string","nullable":true},"htmlContent":{"type":"string","nullable":true},"textContent":{"type":"string","nullable":true},"audienceList":{"nullable":true,"description":"Free-form JSON."},"segmentRules":{"nullable":true,"description":"Free-form JSON."},"scheduledAt":{"type":"string","format":"date-time","nullable":true}}}}}},"responses":{"201":{"description":"The created draft.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"400":{"description":"Missing a name, or an unknown field was sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/campaigns/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The campaign's id."}],"get":{"summary":"Retrieve a campaign","security":[{"bearerAuth":[]}],"x-scope":"campaigns.read","description":"Includes `htmlContent` and `textContent`.","responses":{"200":{"description":"The campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a draft campaign","security":[{"bearerAuth":[]}],"x-scope":"campaigns.write","description":"Requires the `campaigns.write` scope.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Writable fields only: name, type, subject, previewText, fromName, fromEmail, replyTo, templateId, htmlContent, textContent, audienceList, segmentRules, scheduledAt. An unknown field is rejected, not ignored — silently dropping one is how an integration \"works\" while never saving what its author thought it was saving.","properties":{"name":{"type":"string","nullable":true},"type":{"type":"string","nullable":true},"subject":{"type":"string","nullable":true},"previewText":{"type":"string","nullable":true},"fromName":{"type":"string","nullable":true},"fromEmail":{"type":"string","nullable":true},"replyTo":{"type":"string","nullable":true},"templateId":{"type":"string","nullable":true},"htmlContent":{"type":"string","nullable":true},"textContent":{"type":"string","nullable":true},"audienceList":{"nullable":true,"description":"Free-form JSON."},"segmentRules":{"nullable":true,"description":"Free-form JSON."},"scheduledAt":{"type":"string","format":"date-time","nullable":true}}}}}},"responses":{"200":{"description":"The updated draft.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It has been sent, so it can no longer be edited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete a draft campaign","security":[{"bearerAuth":[]}],"x-scope":"campaigns.delete","description":"Requires the `campaigns.delete` scope.","responses":{"200":{"description":"Deleted."},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A sent campaign is a record of what went out and cannot be deleted. Archive it instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/campaigns/{id}/send":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The campaign's id."}],"post":{"summary":"Send a campaign","security":[{"bearerAuth":[]}],"x-scope":"campaigns.send","description":"Accepted for sending, then delivered on a queue — hence 202 rather than 200.\n\nThe send runs with the authority of the person who created the key, so a key can never send if its owner may not. When that person is offboarded, the key stops being able to send with them.\n\nEmail only today. SMS, push and WhatsApp campaigns are refused with `channel_not_supported` rather than half-sent.","responses":{"202":{"description":"Accepted for sending."},"400":{"description":"Not an email campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already sent or in flight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/flows":{"get":{"summary":"List flows","security":[{"bearerAuth":[]}],"x-scope":"flows.read","description":"Requires the `flows.read` scope.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"How many to return. Above 200 is an error, not a silent clamp."},{"name":"starting_after","in":"query","required":false,"schema":{"type":"string"},"description":"The `next_cursor` from the previous page. Cursor, not offset — an offset page silently skips rows when data is written while you iterate."},{"name":"order","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Oldest first (`asc`, the default) or newest first (`desc`). **Use `desc` for anything that polls.** A poller reads the first page and deduplicates by id, so with the default order a newly created row lands at the end of the last page and the poll stops seeing new records once you have more than one page — without erroring."},{"name":"status","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of flows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowList"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/flows/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The flow's id."}],"get":{"summary":"Retrieve a flow","security":[{"bearerAuth":[]}],"x-scope":"flows.read","description":"Requires the `flows.read` scope.","responses":{"200":{"description":"The flow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Flow"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such flow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/flows/{id}/enroll":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The flow's id."}],"post":{"summary":"Enroll a contact in a flow","security":[{"bearerAuth":[]}],"x-scope":"flows.enroll","description":"Idempotent: enrolling someone already active in the flow returns the existing enrollment with `created: false` and 200, so a retry after a timeout cannot mail them twice.\n\nFlows have no create or update endpoint on purpose — a flow definition is a graph the builder validates, and mutating it through the API would strand you with a flow whose failure mode is contacts silently not progressing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"One of contactId or email.","properties":{"contactId":{"type":"string"},"email":{"type":"string"}}}}}},"responses":{"200":{"description":"Already enrolled and active; the existing enrollment is returned."},"201":{"description":"Enrolled."},"400":{"description":"Neither contactId nor email was supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No key, or the key is invalid, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this operation needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such flow, or no contact matched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The flow is not ACTIVE, so it accepts no enrollments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}