# BioFlow > BioFlow is a link-in-bio page with a built-in AI agent. You describe what you > want in chat; the agent designs the page, wires up integrations, and keeps > the content fresh by syncing your latest videos, products, posts, and events. > Plans: Free $0 (free forever), Creator $12/mo, Pro $29/mo. BioFlow is made by Devino (devino.ca). The app lives at app.getbioflow.com; public pages are served at getbioflow.com/your-handle. ## Product - [Home](https://getbioflow.com/): what BioFlow is and how the AI agent builds and maintains a bio page - [Pricing](https://getbioflow.com/pricing): full plan comparison — Free, Creator, and Pro with a per-feature matrix - [FAQ](https://getbioflow.com/faq): common questions about the agent, importing, domains, billing, and trials - [Integrations](https://getbioflow.com/integrations): the 9 live integrations (YouTube, Shopify, Instagram, TikTok, Calendly, Cal.com, Mailchimp, Kit (formerly ConvertKit), RSS) ## Integration details - [YouTube](https://getbioflow.com/integrations/youtube): Your latest uploads appear on your bio page automatically — titles, thumbnails, and view counts included. - [Shopify](https://getbioflow.com/integrations/shopify): Your product catalog on your bio page — prices, images, and availability stay in sync with your store. - [Instagram](https://getbioflow.com/integrations/instagram): Your recent posts, embedded on your page as a live grid that updates itself. - [TikTok](https://getbioflow.com/integrations/tiktok): Your newest TikTok videos on your bio page, with covers and view counts, without re-posting anything. - [Calendly](https://getbioflow.com/integrations/calendly): Visitors book you directly from your bio page — your event types stay in sync with Calendly. - [Cal.com](https://getbioflow.com/integrations/cal-com): Open-source scheduling on your bio page — Cal.com event types synced and bookable inline. - [Mailchimp](https://getbioflow.com/integrations/mailchimp): Collect subscribers from your bio page straight into your Mailchimp audience. - [Kit (formerly ConvertKit)](https://getbioflow.com/integrations/kit): Grow your Kit list from your bio page — forms and subscriber counts synced automatically. - [RSS](https://getbioflow.com/integrations/rss): Any blog or podcast with a feed becomes a self-updating section of your bio page. ## Comparisons - [BioFlow vs Linktree](https://getbioflow.com/compare/linktree): honest, dated comparison of plans and features - [BioFlow vs Beacons](https://getbioflow.com/compare/beacons): honest, dated comparison of plans and features ## AI agents & MCP BioFlow ships a remote MCP (Model Context Protocol) server at https://app.getbioflow.com/api/mcp (Streamable HTTP). Any MCP-capable agent — Claude, Claude Code, ChatGPT, Copilot, MCP Inspector — can connect via OAuth 2.1 with PKCE and dynamic client registration (no pre-shared API keys). Tokens are workspace-scoped; publishing is off by default and two-step confirmed. - [MCP server docs](https://getbioflow.com/mcp): how to connect an agent, the 12 tools, OAuth scopes, and safety rails ## API BioFlow ships a public REST API at https://app.getbioflow.com/v1 (Creator 10,000 requests/mo, Pro 100,000 requests/mo; Free has no API access). Auth is an API key (bf_live_/bf_test_); errors are RFC 9457 problem+json with stable codes. Every developer page also serves a markdown twin at {page-url}.md (e.g. https://getbioflow.com/docs/api.md). - [Developer platform](https://getbioflow.com/developers): API + SDK + webhooks + MCP overview - [API docs](https://getbioflow.com/docs/api): quickstart, auth, rate limits, pagination, idempotency, all operations - [Interactive API reference](https://getbioflow.com/docs/api/reference): rendered from the OpenAPI 3.1 spec - [Error codes](https://getbioflow.com/docs/api#errors): the 14-code RFC 9457 problem registry, one page per code - [Webhooks guide](https://getbioflow.com/docs/api/webhooks): Standard Webhooks v1 signing, events, retries, replay - [OpenAPI 3.1 spec](https://getbioflow.com/docs/api/openapi.json): machine-readable surface description - [AI tooling index](https://getbioflow.com/docs/ai): llms.txt, llms-full.txt, .md twins, MCP connect - [Changelog](https://getbioflow.com/changelog): dated platform updates (RSS at https://getbioflow.com/changelog/rss.xml) - [Full API reference as plain text](https://getbioflow.com/llms-full.txt): every operation and error code in one document --- # BioFlow API — full reference Base URL: https://app.getbioflow.com/v1 · Auth: `Authorization: Bearer bf_live_...` or `x-api-key` · Errors: RFC 9457 problem+json with a stable `code` · Pagination: `{ data, has_more, next_cursor }` · Consequential POSTs accept an `Idempotency-Key` header. Human docs: https://getbioflow.com/docs/api ## Get analytics summary (`GET /v1/analytics/summary`) - Operation ID: `getAnalyticsSummary` - Docs: https://getbioflow.com/docs/api/operations/get-analytics-summary - Required scope: `analytics:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Workspace analytics for the last 7/30/90 days: views, clicks, unique visitors, CTR, top links, top referrers, and tip revenue. Requires scope: analytics:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `range_days` | query | integer | no | | ### Response 200 — The analytics summary | Field | Type | Description | | --- | --- | --- | | `range_days` | integer | | | `totals` | object | | | `top_links` | array | | | `top_referrers` | array | | | `tips` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/analytics/summary" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/analytics/summary", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## List contacts (`GET /v1/contacts`) - Operation ID: `listContacts` - Docs: https://getbioflow.com/docs/api/operations/list-contacts - Required scope: `contacts:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Captured contacts and leads (newsletter signups, form submissions, gated downloads), newest first. Cursor-paginated with REAL database keyset cursors — safe at any collection size. Requires scope: contacts:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | | | `after` | query | string | no | Opaque cursor from a previous response | ### Response 200 — A page of contacts | Field | Type | Description | | --- | --- | --- | | `data` | array | | | `has_more` | boolean | | | `next_cursor` | string | null | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/contacts" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/contacts", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## List files (`GET /v1/files`) - Operation ID: `listFiles` - Docs: https://getbioflow.com/docs/api/operations/list-files - Required scope: `files:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required The workspace's uploaded files (name, MIME type, size, public path), newest first. Pass a file's public_path as file_url to Add block to wire it into IMAGE/FILE/CAROUSEL/PRODUCT blocks. Requires scope: files:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | | | `after` | query | string | no | Opaque cursor from a previous response | ### Response 200 — A page of files | Field | Type | Description | | --- | --- | --- | | `data` | array | | | `has_more` | boolean | | | `next_cursor` | string | null | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/files" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/files", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## List pages (`GET /v1/pages`) - Operation ID: `listPages` - Docs: https://getbioflow.com/docs/api/operations/list-pages - Required scope: `pages:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Every bio page in the workspace, newest-updated first. Cursor-paginated. Requires scope: pages:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | | | `after` | query | string | no | Opaque cursor from a previous response | ### Response 200 — A page of pages | Field | Type | Description | | --- | --- | --- | | `data` | array | | | `has_more` | boolean | | | `next_cursor` | string | null | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Create a page (`POST /v1/pages`) - Operation ID: `createPage` - Docs: https://getbioflow.com/docs/api/operations/create-page - Required scope: `pages:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Create a new bio page as an UNPUBLISHED draft — the same path as the dashboard's New page dialog (slug derived from the title and deduped). Nothing goes live until Publish page. Requires scope: pages:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | | | `theme_preset_key` | string | no | One of the 13 canonical theme preset keys | | `header_layout` | classic | banner | hero | cutout | no | | ### Response 201 — The created page | Field | Type | Description | | --- | --- | --- | | `id` | string | | | `title` | string | | | `description` | string | | | `slug` | string | | | `status` | DRAFT | PUBLISHED | ARCHIVED | | | `locale` | string | | | `public_url` | string | null | Live URL when routed; null for unrouted drafts | | `theme` | object | | | `draft` | object | | | `published` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" \ -H "Content-Type: application/json" \ -d '{ "title": "string", "theme_preset_key": "string", "header_layout": "classic" }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "title": "string", "theme_preset_key": "string", "header_layout": "classic" }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Delete a page (`DELETE /v1/pages/{page_id}`) - Operation ID: `deletePage` - Docs: https://getbioflow.com/docs/api/operations/delete-page - Required scope: `pages:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Permanently delete a page, its blocks, versions, and route. This cannot be undone. Requires scope: pages:write. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_id` | path | string | yes | Page ID from List pages | ### Response 204 — Deleted — no content ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages/PAGE_ID" \ -X DELETE \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages/PAGE_ID", { method: "DELETE", headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Get a page (`GET /v1/pages/{page_id}`) - Operation ID: `getPage` - Docs: https://getbioflow.com/docs/api/operations/get-page - Required scope: `pages:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required One page's draft AND published state: ordered draft block refs (IDs for the write endpoints), theme, route, and the draft.updated_at concurrency stamp every write requires. Requires scope: pages:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_id` | path | string | yes | Page ID from List pages | ### Response 200 — The page | Field | Type | Description | | --- | --- | --- | | `id` | string | | | `title` | string | | | `description` | string | | | `slug` | string | | | `status` | DRAFT | PUBLISHED | ARCHIVED | | | `locale` | string | | | `public_url` | string | null | Live URL when routed; null for unrouted drafts | | `theme` | object | | | `draft` | object | | | `published` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages/PAGE_ID" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages/PAGE_ID", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Update a page draft (`PATCH /v1/pages/{page_id}`) - Operation ID: `updatePage` - Docs: https://getbioflow.com/docs/api/operations/update-page - Required scope: `pages:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Merge-patch the DRAFT: title, description, theme preset, per-block data patches, and/or full reorder via block_order — everything unmentioned is preserved. Requires expected_updated_at; a stale stamp is refused with 409 stale_snapshot. Requires scope: pages:write. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_id` | path | string | yes | Page ID from List pages | ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `expected_updated_at` | string | yes | Concurrency stamp: the draft.updated_at you last read. A mismatch is refused with 409 stale_snapshot — re-read the page and retry. | | `title` | string | no | | | `description` | string | no | | | `theme_preset_key` | string | no | | | `block_updates` | array | no | | | `block_order` | array | no | Every existing block ID exactly once, in the new top-to-bottom order | ### Response 200 — The updated page | Field | Type | Description | | --- | --- | --- | | `id` | string | | | `title` | string | | | `description` | string | | | `slug` | string | | | `status` | DRAFT | PUBLISHED | ARCHIVED | | | `locale` | string | | | `public_url` | string | null | Live URL when routed; null for unrouted drafts | | `theme` | object | | | `draft` | object | | | `published` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages/PAGE_ID" \ -X PATCH \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "expected_updated_at": "string", "title": "string", "description": "string", "theme_preset_key": "string", "block_updates": [ { "block_id": "string", "data": {} } ], "block_order": [ "string" ] }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages/PAGE_ID", { method: "PATCH", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "expected_updated_at": "string", "title": "string", "description": "string", "theme_preset_key": "string", "block_updates": [ { "block_id": "string", "data": {} } ], "block_order": [ "string" ] }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Add a block (`POST /v1/pages/{page_id}/blocks`) - Operation ID: `addBlock` - Docs: https://getbioflow.com/docs/api/operations/add-block - Required scope: `pages:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Append ONE block to the page DRAFT, preserving every existing block byte-for-byte. Optionally wire an uploaded file (public_path from List files) into IMAGE/FILE/CAROUSEL/PRODUCT blocks. Requires expected_updated_at. Requires scope: pages:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_id` | path | string | yes | Page ID from List pages | ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `expected_updated_at` | string | yes | Concurrency stamp: the draft.updated_at you last read. A mismatch is refused with 409 stale_snapshot — re-read the page and retry. | | `kind` | string | yes | Block kind, e.g. LINK | | `data` | object | no | | | `file_url` | string | no | public_path of a workspace file from List files | | `file_name` | string | no | | ### Response 201 — The page with the new block | Field | Type | Description | | --- | --- | --- | | `id` | string | | | `title` | string | | | `description` | string | | | `slug` | string | | | `status` | DRAFT | PUBLISHED | ARCHIVED | | | `locale` | string | | | `public_url` | string | null | Live URL when routed; null for unrouted drafts | | `theme` | object | | | `draft` | object | | | `published` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages/PAGE_ID/blocks" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" \ -H "Content-Type: application/json" \ -d '{ "expected_updated_at": "string", "kind": "string", "data": {}, "file_url": "string", "file_name": "string" }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages/PAGE_ID/blocks", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "expected_updated_at": "string", "kind": "string", "data": {}, "file_url": "string", "file_name": "string" }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Remove a block (`DELETE /v1/pages/{page_id}/blocks/{block_id}`) - Operation ID: `removeBlock` - Docs: https://getbioflow.com/docs/api/operations/remove-block - Required scope: `pages:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Delete one block from the page DRAFT, preserving everything else. Removing the last remaining block is refused. Requires expected_updated_at (query parameter). Requires scope: pages:write. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_id` | path | string | yes | | | `block_id` | path | string | yes | Block ID from the page's draft.blocks | | `expected_updated_at` | query | string | yes | Concurrency stamp: the draft.updated_at you last read. A mismatch is refused with 409 stale_snapshot — re-read the page and retry. | ### Response 200 — The page without the block | Field | Type | Description | | --- | --- | --- | | `id` | string | | | `title` | string | | | `description` | string | | | `slug` | string | | | `status` | DRAFT | PUBLISHED | ARCHIVED | | | `locale` | string | | | `public_url` | string | null | Live URL when routed; null for unrouted drafts | | `theme` | object | | | `draft` | object | | | `published` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages/PAGE_ID/blocks/BLOCK_ID" \ -X DELETE \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages/PAGE_ID/blocks/BLOCK_ID", { method: "DELETE", headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Publish a page (`POST /v1/pages/{page_id}/publish`) - Operation ID: `publishPage` - Docs: https://getbioflow.com/docs/api/operations/publish-page - Required scope: `publish` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Publish the page's current DRAFT to its live public route — the same code path as the editor's Publish button. Pass starts_at to schedule the publish instead of applying it now. Requires the publish scope, granted separately at key creation. Requires scope: publish. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page_id` | path | string | yes | Page ID from List pages | ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `starts_at` | string (ISO 8601) | no | Omit to publish now; an ISO instant schedules the publish instead | ### Response 200 — The published (or scheduled) page | Field | Type | Description | | --- | --- | --- | | `page` | object | | | `scheduled_for` | string (ISO 8601) | null | Set when the publish was scheduled, not applied | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/pages/PAGE_ID/publish" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" \ -H "Content-Type: application/json" \ -d '{ "starts_at": "2026-08-01T12:00:00.000Z" }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/pages/PAGE_ID/publish", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "starts_at": "2026-08-01T12:00:00.000Z" }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Get API usage (`GET /v1/usage`) - Operation ID: `getUsage` - Docs: https://getbioflow.com/docs/api/operations/get-usage - Required scope: `pages:read` or `pages:write` or `analytics:read` or `contacts:read` or `files:read` or `publish` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: free (does not count against the monthly quota) - Idempotent: no `Idempotency-Key` required Current billing-period API usage: requests used/remaining against the plan's monthly quota, the reset instant, and the per-key burst limit. Polling this endpoint never consumes quota. Requires scope: pages:read or pages:write or analytics:read or contacts:read or files:read or publish. Plan entitlement: public_api (Creator and Pro). ### Response 200 — Current usage | Field | Type | Description | | --- | --- | --- | | `plan` | string | Workspace plan id, e.g. CREATOR | | `period_start` | string (ISO 8601) | | | `period_end` | string (ISO 8601) | When the meter resets | | `meters` | array | | | `burst` | object | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/usage" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/usage", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## List webhook endpoints (`GET /v1/webhook-endpoints`) - Operation ID: `listWebhookEndpoints` - Docs: https://getbioflow.com/docs/api/operations/list-webhook-endpoints - Required scope: `webhooks:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Every webhook endpoint in the workspace (at most 10 — no pagination). Signing secrets are never included. Requires scope: webhooks:read. Plan entitlement: public_api (Creator and Pro). ### Response 200 — The endpoints | Field | Type | Description | | --- | --- | --- | | `data` | array | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Create a webhook endpoint (`POST /v1/webhook-endpoints`) - Operation ID: `createWebhookEndpoint` - Docs: https://getbioflow.com/docs/api/operations/create-webhook-endpoint - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Register a public HTTPS URL for event delivery. The URL is SSRF-screened, then must accept a SIGNED endpoint.test event with 2xx before anything is stored. The response carries the whsec_ signing secret exactly once. Deliveries are signed per Standard Webhooks (webhook-id / webhook-timestamp / webhook-signature over `id.timestamp.rawBody`), retried with exponential backoff for ~41 hours, and the endpoint auto-disables after 20 consecutive failures. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | Public HTTPS URL on port 443. Verified with a signed endpoint.test event before the endpoint is created — respond 2xx. | | `event_types` | array | yes | | ### Response 201 — The created endpoint and its signing secret (shown once) | Field | Type | Description | | --- | --- | --- | | `endpoint` | object | | | `secret` | string | The whsec_ signing secret — shown ONCE, store it now. Verify webhook-signature with it per Standard Webhooks. | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/bioflow", "event_types": [ "contact.created" ] }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "url": "https://example.com/hooks/bioflow", "event_types": [ "contact.created" ] }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Delete a webhook endpoint (`DELETE /v1/webhook-endpoints/{endpoint_id}`) - Operation ID: `deleteWebhookEndpoint` - Docs: https://getbioflow.com/docs/api/operations/delete-webhook-endpoint - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Permanently delete the endpoint and its delivery history. This cannot be undone. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | ### Response 204 — Deleted — no content ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID" \ -X DELETE \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID", { method: "DELETE", headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Get a webhook endpoint (`GET /v1/webhook-endpoints/{endpoint_id}`) - Operation ID: `getWebhookEndpoint` - Docs: https://getbioflow.com/docs/api/operations/get-webhook-endpoint - Required scope: `webhooks:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Requires scope: webhooks:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | ### Response 200 — The endpoint | Field | Type | Description | | --- | --- | --- | | `id` | string | Stable opaque endpoint ID | | `url` | string | | | `status` | ACTIVE | DISABLED | | | `disabled_reason` | string | null | | | `event_types` | array | | | `consecutive_failures` | integer | | | `last_success_at` | string (ISO 8601) | null | | | `last_failure_at` | string (ISO 8601) | null | | | `previous_secret_expires_at` | string (ISO 8601) | null | While set, the pre-rotation secret still signs deliveries (second signature) | | `created_at` | string (ISO 8601) | | | `updated_at` | string (ISO 8601) | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Update a webhook endpoint (`PATCH /v1/webhook-endpoints/{endpoint_id}`) - Operation ID: `updateWebhookEndpoint` - Docs: https://getbioflow.com/docs/api/operations/update-webhook-endpoint - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required Change the URL (re-verified with a signed test event first), the subscribed event types, or enable/disable delivery. Re-enabling resets the consecutive-failure counter; missed events can then be re-queued with Replay failed deliveries. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | no | New URL — re-verified with a signed test event first | | `event_types` | array | no | | | `enabled` | boolean | no | true re-enables a disabled endpoint (and resets its failure count); false disables it | ### Response 200 — The updated endpoint | Field | Type | Description | | --- | --- | --- | | `id` | string | Stable opaque endpoint ID | | `url` | string | | | `status` | ACTIVE | DISABLED | | | `disabled_reason` | string | null | | | `event_types` | array | | | `consecutive_failures` | integer | | | `last_success_at` | string (ISO 8601) | null | | | `last_failure_at` | string (ISO 8601) | null | | | `previous_secret_expires_at` | string (ISO 8601) | null | While set, the pre-rotation secret still signs deliveries (second signature) | | `created_at` | string (ISO 8601) | | | `updated_at` | string (ISO 8601) | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID" \ -X PATCH \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/bioflow", "event_types": [ "contact.created" ], "enabled": true }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID", { method: "PATCH", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "url": "https://example.com/hooks/bioflow", "event_types": [ "contact.created" ], "enabled": true }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## List deliveries (`GET /v1/webhook-endpoints/{endpoint_id}/deliveries`) - Operation ID: `listWebhookDeliveries` - Docs: https://getbioflow.com/docs/api/operations/list-webhook-deliveries - Required scope: `webhooks:read` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: no `Idempotency-Key` required The endpoint's delivery log, newest first, cursor-paginated, with per-attempt status codes, bounded error summaries, and the next automatic retry time. Requires scope: webhooks:read. Plan entitlement: public_api (Creator and Pro). ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | | `limit` | query | integer | no | | | `after` | query | string | no | Opaque cursor from a previous response | | `status` | query | PENDING | SUCCEEDED | FAILED | no | | ### Response 200 — A page of deliveries | Field | Type | Description | | --- | --- | --- | | `data` | array | | | `has_more` | boolean | | | `next_cursor` | string | null | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/deliveries" \ -H "Authorization: Bearer bf_live_YOUR_KEY" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/deliveries", { headers: { Authorization: "Bearer bf_live_YOUR_KEY", }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Resend a delivery (`POST /v1/webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/resend`) - Operation ID: `resendWebhookDelivery` - Docs: https://getbioflow.com/docs/api/operations/resend-webhook-delivery - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Re-queue ONE delivery for immediate redelivery, restarting its retry ladder. The webhook-id stays the same — consumers deduplicate on it. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | | | `delivery_id` | path | string | yes | Delivery ID from List deliveries | ### Response 200 — The re-queued delivery | Field | Type | Description | | --- | --- | --- | | `id` | string | | | `event_id` | string | | | `event_type` | string | | | `status` | PENDING | SUCCEEDED | FAILED | | | `attempt_count` | integer | | | `next_attempt_at` | string (ISO 8601) | null | When the next automatic attempt is due (PENDING only) | | `last_status_code` | integer | null | | | `created_at` | string (ISO 8601) | | | `attempts` | array | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/deliveries/DELIVERY_ID/resend" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/deliveries/DELIVERY_ID/resend", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Replay failed deliveries (`POST /v1/webhook-endpoints/{endpoint_id}/replay`) - Operation ID: `replayWebhookDeliveries` - Docs: https://getbioflow.com/docs/api/operations/replay-webhook-deliveries - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Bulk recovery: re-queue every FAILED delivery created since the given instant (last 30 days). Use after re-enabling an auto-disabled endpoint. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | ### Request body (application/json) | Field | Type | Required | Description | | --- | --- | --- | --- | | `since` | string (ISO 8601) | yes | Re-queue every FAILED delivery created at or after this instant (last 30 days) | ### Response 200 — How many deliveries were re-queued | Field | Type | Description | | --- | --- | --- | | `replayed` | integer | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/replay" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" \ -H "Content-Type: application/json" \ -d '{ "since": "2026-08-01T12:00:00.000Z" }' ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/replay", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "since": "2026-08-01T12:00:00.000Z" }), }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Rotate the signing secret (`POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret`) - Operation ID: `rotateWebhookSecret` - Docs: https://getbioflow.com/docs/api/operations/rotate-webhook-secret - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Mint a NEW whsec_ secret (returned once). For 24 hours deliveries carry TWO signatures — the new and the previous secret — so you can roll your verifier without dropping events. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | ### Response 200 — The endpoint and its new secret (shown once) | Field | Type | Description | | --- | --- | --- | | `endpoint` | object | | | `secret` | string | The NEW whsec_ signing secret — shown once, store it now | | `previous_secret_expires_at` | string (ISO 8601) | Until then the old secret also signs (second signature) | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/rotate-secret" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/rotate-secret", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error ## Send a test event (`POST /v1/webhook-endpoints/{endpoint_id}/test`) - Operation ID: `testWebhookEndpoint` - Docs: https://getbioflow.com/docs/api/operations/test-webhook-endpoint - Required scope: `webhooks:write` - Plan entitlement: `public_api` (Creator and Pro plans) - Quota cost: 1 request - Idempotent: yes — send an `Idempotency-Key` header Synchronously send a signed endpoint.test event to the endpoint and report the outcome. Does not touch the delivery log or failure counters. Requires scope: webhooks:write. Plan entitlement: public_api (Creator and Pro). Consequential operation — send an Idempotency-Key header. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | Endpoint ID from List webhook endpoints | ### Response 200 — The test outcome | Field | Type | Description | | --- | --- | --- | | `delivered` | boolean | | | `status_code` | integer | null | | | `error` | string | null | | ### Example (cURL) ```bash curl "https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/test" \ -X POST \ -H "Authorization: Bearer bf_live_YOUR_KEY" \ -H "Idempotency-Key: a-unique-id-per-attempt" ``` ### Example (TypeScript) ```ts const res = await fetch("https://app.getbioflow.com/v1/webhook-endpoints/ENDPOINT_ID/test", { method: "POST", headers: { Authorization: "Bearer bf_live_YOUR_KEY", "Idempotency-Key": crypto.randomUUID(), }, }); if (!res.ok) { // RFC 9457 problem+json — switch on the stable `code` field. const problem = await res.json(); throw new Error(`BioFlow API ${problem.code}: ${problem.title}`); } const data = await res.json(); ``` ### Errors - `invalid_request` (400): The request body or parameters failed validation — https://getbioflow.com/docs/api/errors/invalid-request - `invalid_api_key` (401): Missing, malformed, unknown, disabled, or expired API key — https://getbioflow.com/docs/api/errors/invalid-api-key - `insufficient_scope` (403): The API key was not granted the required scope — https://getbioflow.com/docs/api/errors/insufficient-scope - `feature_not_enabled` (403): The workspace plan does not include public API access — https://getbioflow.com/docs/api/errors/feature-not-enabled - `test_key_read_only` (403): Test-mode keys are restricted to read operations — https://getbioflow.com/docs/api/errors/test-key-read-only - `rate_limited` (429): Per-key rate limit exceeded — https://getbioflow.com/docs/api/errors/rate-limited - `quota_exhausted` (429): Monthly API quota exhausted — https://getbioflow.com/docs/api/errors/quota-exhausted - `internal_error` (500): Something went wrong on our side — https://getbioflow.com/docs/api/errors/internal-error # Error codes (14) ## `invalid_request` — HTTP 400 The request body or parameters failed validation. - Problem `type` URI: https://getbioflow.com/docs/api/errors/invalid-request - Docs: https://getbioflow.com/docs/api/errors/invalid-request - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `invalid_api_key` — HTTP 401 Missing, malformed, unknown, disabled, or expired API key. - Problem `type` URI: https://getbioflow.com/docs/api/errors/invalid-api-key - Docs: https://getbioflow.com/docs/api/errors/invalid-api-key - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `insufficient_scope` — HTTP 403 The API key was not granted the required scope. - Problem `type` URI: https://getbioflow.com/docs/api/errors/insufficient-scope - Docs: https://getbioflow.com/docs/api/errors/insufficient-scope - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `feature_not_enabled` — HTTP 403 The workspace plan does not include public API access. - Problem `type` URI: https://getbioflow.com/docs/api/errors/feature-not-enabled - Docs: https://getbioflow.com/docs/api/errors/feature-not-enabled - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `test_key_read_only` — HTTP 403 Test-mode keys are restricted to read operations. - Problem `type` URI: https://getbioflow.com/docs/api/errors/test-key-read-only - Docs: https://getbioflow.com/docs/api/errors/test-key-read-only - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `resource_not_found` — HTTP 404 No resource with that ID exists in this workspace. - Problem `type` URI: https://getbioflow.com/docs/api/errors/resource-not-found - Docs: https://getbioflow.com/docs/api/errors/resource-not-found - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `stale_snapshot` — HTTP 409 The draft changed since it was last read. - Problem `type` URI: https://getbioflow.com/docs/api/errors/stale-snapshot - Docs: https://getbioflow.com/docs/api/errors/stale-snapshot - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `idempotency_in_progress` — HTTP 409 A request with this Idempotency-Key is still executing. - Problem `type` URI: https://getbioflow.com/docs/api/errors/idempotency-in-progress - Docs: https://getbioflow.com/docs/api/errors/idempotency-in-progress - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `idempotency_key_reused` — HTTP 422 This Idempotency-Key was already used with a different request. - Problem `type` URI: https://getbioflow.com/docs/api/errors/idempotency-key-reused - Docs: https://getbioflow.com/docs/api/errors/idempotency-key-reused - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `endpoint_verification_failed` — HTTP 422 The webhook endpoint did not accept the signed test event. - Problem `type` URI: https://getbioflow.com/docs/api/errors/endpoint-verification-failed - Docs: https://getbioflow.com/docs/api/errors/endpoint-verification-failed - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `endpoint_limit_reached` — HTTP 422 The workspace webhook endpoint limit was reached. - Problem `type` URI: https://getbioflow.com/docs/api/errors/endpoint-limit-reached - Docs: https://getbioflow.com/docs/api/errors/endpoint-limit-reached - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `rate_limited` — HTTP 429 Per-key rate limit exceeded. - Problem `type` URI: https://getbioflow.com/docs/api/errors/rate-limited - Docs: https://getbioflow.com/docs/api/errors/rate-limited - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `quota_exhausted` — HTTP 429 Monthly API quota exhausted. - Problem `type` URI: https://getbioflow.com/docs/api/errors/quota-exhausted - Docs: https://getbioflow.com/docs/api/errors/quota-exhausted - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`. ## `internal_error` — HTTP 500 Something went wrong on our side. - Problem `type` URI: https://getbioflow.com/docs/api/errors/internal-error - Docs: https://getbioflow.com/docs/api/errors/internal-error - Errors are RFC 9457 `application/problem+json`; branch on the stable `code` field, never on `title`/`detail`.