openapi: 3.1.0
info:
title: Primitive Account Functions API
version: 1.0.0
description: "Primitive is email infrastructure for AI agents. The Primitive API lets you manage domains, emails, webhook endpoints,\nfilters, and account settings programmatically.\n\n## Authentication\n\nMost endpoints require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer prim_<your_api_key>\nAuthorization: Bearer prim_oat_<oauth_access_token>\n```\n\nAPI keys and OAuth access tokens are org-scoped. Create and manage them in your dashboard\nunder Settings > API Keys. CLI login plus CLI/agent signup endpoints\nexplicitly declare `security: []`; they do not require an API key because\nthey are used to create OAuth CLI sessions.\n\n## Rate Limiting\n\nThe API enforces a sliding window rate limit of **120 requests per\n60 seconds** per organization. When exceeded, the API returns `429`\nwith a `Retry-After` header indicating how many seconds to wait.\n\n## Pagination\n\nList endpoints use cursor-based pagination. Responses include a\n`meta` object with `total`, `limit`, and `cursor` fields. Pass the\n`cursor` value as a query parameter to fetch the next page. When\n`cursor` is `null`, there are no more results.\n\n## Response Format\n\nAll responses use a consistent envelope:\n\n```json\n{\n \"success\": true,\n \"data\": { ... },\n \"meta\": { \"total\": 42, \"limit\": 50, \"cursor\": \"...\" }\n}\n```\n\nErrors follow the same pattern:\n\n```json\n{\n \"success\": false,\n \"error\": { \"code\": \"not_found\", \"message\": \"Email not found\" }\n}\n```\n\n## Webhook signing\n\nOutbound webhook deliveries (configured via the `endpoints` API)\nare signed so receivers can verify they came from Primitive and\nhave not been tampered with in transit. The signing scheme is\ndeliberately simple so it can be reimplemented in any language\nin a few lines. The Node SDK's `verifyWebhookSignature` helper\nis the reference implementation; the wire details below let you\nwrite a verifier in Python, Go, Ruby, etc. without reading our\nsource.\n\n**Header**: `Primitive-Signature: t=<unix-seconds>,v1=<hex>`\n\nA legacy `MyMX-Signature` header is also sent on every delivery\nwith the same value, retained for back-compatibility with\nintegrations written before the rename. New code should read\n`Primitive-Signature`.\n\n**Signed string**: `${timestamp}.${rawBody}` where `timestamp`\nis the Unix-seconds integer from the `t=` parameter and\n`rawBody` is the exact bytes of the HTTP request body BEFORE\nany JSON decoding. Verify against the raw body, not a\nre-serialized parse, or you will silently mismatch on\ninsignificant whitespace.\n\n**Signature**: HMAC-SHA256 of the signed string, hex-encoded\n(lowercase). Use the account's webhook secret as the HMAC key,\nas a UTF-8 byte sequence.\n\n**Secret**: returned by `GET /account/webhook-secret`. The\nstring looks base64-shaped (e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`)\nbut is NOT base64; use it AS-IS as a UTF-8 string for the HMAC\nkey. Base64-decoding before HMAC will silently produce\nmismatched signatures.\n\n**Tolerance**: by convention, reject deliveries whose `t=`\ntimestamp is more than 5 minutes off your wall-clock to defend\nagainst replay attacks. The Node SDK's helper enforces this by\ndefault.\n\n**Verification recipe** (any language):\n\n```\n1. Read the raw HTTP body (do not parse).\n2. Read `Primitive-Signature: t=<ts>,v1=<sig>`.\n3. Reject if abs(now - ts) > 300 seconds.\n4. expected = HMAC_SHA256_hex(secret_utf8, f\"{ts}.{rawBody}\")\n5. Constant-time compare expected to sig. Reject if not equal.\n```\n\nFor Node, use `verifyWebhookSignature` from\n`@primitivedotdev/sdk/webhook` (or the higher-level\n`handleWebhook` helper if you want a one-liner). For other\nlanguages, the recipe above is everything you need.\n\nTest deliveries: `POST /endpoints/{id}/test` triggers a fake\ndelivery to your endpoint URL, signed with your real account\nsecret, so you can confirm verification end-to-end without\nneeding real inbound mail. The test response carries the exact\n`signature` header value sent on the wire so you can compare\nstrings directly.\n\n\n## Errors\n\nEvery error response is the same JSON envelope (`{ \"success\": false, \"error\": { \"code\", \"message\" } }`), served as `application/json` with HTTP status codes, following the RFC 7807 problem-details shape. The `error.code` is a stable machine-readable string and `error.message` is human-readable.\n\n## Authorization and roles\n\nAccess is governed by organization role-based access control. Every organization member holds one of three roles — `owner`, `admin`, or `member` — and a credential inherits a role. **API keys** always act at `member` level, regardless of the role of the user who created them, so an API key can never perform owner- or admin-only actions. **OAuth access tokens** act with the authorizing user's current organization role, resolved on each request. Every operation in this spec is part of the member-level surface, so any valid credential can call it. Organization administration that is not part of this API — billing and organization settings — requires an `owner` or `admin` and is performed in the dashboard. Fine-grained per-key scopes (e.g. a send-only or read-only key) are on the roadmap; today the role model is the unit of access control.\n\n## Versioning\n\nThe current stable API is **v1**. All endpoints are served under `/v1/` and are covered by a backward-compatibility guarantee: existing fields and status codes will not change without a deprecation notice.\n\nBreaking changes are announced at least 6 months in advance via changelog and email. Deprecated operations and fields are marked `x-deprecated: true` in the spec and carry a plain-English description of the replacement. The `v1` path prefix is guaranteed stable indefinitely; backward-compatible additions (new optional fields, new endpoints) may be made at any time without a version bump."
contact:
name: Primitive
url: https://primitive.dev
license:
name: Proprietary
url: https://primitive.dev/terms
x-stability-level: stable
x-deprecation-policy: 'Breaking changes are announced at least 6 months in advance. Deprecated fields carry x-deprecated: true. The current stable version is v1.'
servers:
- url: https://api.primitive.dev/v1
description: Canonical API host (PRIMITIVE_API_BASE_URL). Carries every public API operation.
tags:
- name: Functions
description: 'Deploy JavaScript handlers that run on inbound mail. Each function
is a single ESM module whose default export is an object with an
async `fetch(request, env)` method, in the shape of a Workers-style
handler. Primitive signs each delivery and forwards the
`Primitive-Signature` header to the handler; verify the raw request
body with `PRIMITIVE_WEBHOOK_SECRET` before trusting the parsed event.
The `event` field is `email.received` for normal inbound mail, or a
machine-mail type (`email.bounced`, `email.tls_report`,
`email.dmarc_report`, `email.dmarc_failure`) for bounces and reports;
the payload shape is otherwise identical. Code runs on
Primitive''s edge runtime; there is no infrastructure to manage.
Secrets land in `env` as encrypted bindings and are refreshed on
every redeploy.
'
paths:
/functions:
get:
operationId: listFunctions
summary: List functions
description: 'Returns every active (non-deleted) function in the org, newest
first. Each entry carries deploy status and timestamps. To
inspect the source code or deploy errors, use `GET /functions/{id}`.
'
tags:
- Functions
responses:
'200':
description: List of functions
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: array
items:
type: object
description: One row from the function listing.
properties:
id:
type: string
format: uuid
description: Function id, also the script name in the edge runtime.
name:
type: string
description: Slug-style name set on creation. Stable; cannot be changed.
deploy_status:
type: string
enum:
- pending
- deployed
- failed
description: "Lifecycle state of the latest deploy attempt:\n * `pending` — deploy in flight; the runtime has not yet\n confirmed the new bundle is live.\n * `deployed` — the running edge handler is the latest code.\n * `failed` — the most recent deploy attempt failed; the\n previously-live code (if any) is still running. The\n `deploy_error` field carries the error message.\n"
deployed_at:
type:
- string
- 'null'
format: date-time
description: Timestamp of the most recent successful deploy. Null until the first deploy succeeds.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- name
- deploy_status
- created_at
- updated_at
headers:
ratelimit-limit:
description: Maximum number of requests allowed in the current window.
schema:
type: integer
minimum: 1
example: 120
ratelimit-remaining:
description: Remaining requests in the current window.
schema:
type: integer
minimum: 0
example: 118
ratelimit-reset:
description: Unix timestamp (seconds) when the current window resets.
schema:
type: integer
example: 1700000060
ratelimit-policy:
description: Rate-limit policy in `limit;w=seconds` format, e.g. `120;w=60`.
schema:
type: string
example: 120;w=60
'401':
$ref: '#/components/responses/Unauthorized'
description: Invalid or missing API key
security:
- BearerAuth: []
post:
operationId: createFunction
summary: Deploy a function
description: 'Creates and deploys a new function. The handler must be a single
ESM module whose default export is an object with an async
`fetch(request, env)` method (Workers-style). Primitive signs
each delivery and forwards the `Primitive-Signature` header to
the handler. Verify the raw request body with
`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification
the request body parses to a webhook event whose `event` field is
`email.received` for normal inbound mail, or a machine-mail type
(`email.bounced`, `email.tls_report`, `email.dmarc_report`,
`email.dmarc_failure`) for bounces and reports. Code is bundled
before being uploaded; ship a single self-contained file rather
than relying on external imports.
**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`
(optional) is capped at 5 MiB UTF-8, stored with each deployment
attempt, and sent to the runtime so stack traces can resolve to
original source files.
**Routing.** On successful deploy, the function code is live
in the runtime, but inbound mail will not reach it until at
least one route is bound. Routes are managed from the Primitive
dashboard. A `deploy_status` of `deployed` means the script is
installed, not that the function is receiving mail. The
internal runtime URL is not returned by the API and is not a
customer-facing integration surface.
**Secrets.** New functions ship with the managed secrets
(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,
`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via
`POST /functions/{id}/secrets`; secret writes only land in the
running handler on the next redeploy.
'
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
name:
type: string
pattern: ^[a-z0-9_-]{1,64}$
description: 'Slug-style name. Lowercase letters, digits, hyphens, and
underscores. 1 to 64 characters. Must be unique within the
org; a 409 is returned on collision.
'
code:
type: string
minLength: 1
maxLength: 1048576
description: 'Pre-built handler as a single ESM module. Up to 1 MiB UTF-8.
Must export a default `{ async fetch(req, env, ctx) { ... } }`
object. Provide either `code` or `files`, not both.
'
sourceMap:
type: string
minLength: 1
maxLength: 5242880
description: 'Optional source map for the bundle. Up to 5 MiB UTF-8.
Stored with the deployment attempt and sent to the runtime
to symbolicate stack traces in the function''s logs. Only
valid with `code`.
'
files:
type: object
additionalProperties:
type: string
description: 'Source files for a managed build, as a map of path to file
contents (for example {"package.json": "...",
"src/index.ts": "..."}). Provide this INSTEAD of `code` to
have the server install dependencies and bundle the source
for the Workers runtime before deploying. Include a
package.json (its `dependencies` are installed). Provide
either `code` or `files`, not both.
'
required:
- name
responses:
'201':
description: Function created and deployed
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: Returned by POST /functions on a successful deploy.
properties:
id:
type: string
format: uuid
name:
type: string
deploy_status:
type: string
enum:
- pending
- deployed
- failed
description: "Lifecycle state of the latest deploy attempt:\n * `pending` — deploy in flight; the runtime has not yet\n confirmed the new bundle is live.\n * `deployed` — the running edge handler is the latest code.\n * `failed` — the most recent deploy attempt failed; the\n previously-live code (if any) is still running. The\n `deploy_error` field carries the error message.\n"
required:
- id
- name
- deploy_status
'400':
$ref: '#/components/responses/ValidationError'
description: Invalid request parameters or customer-correctable deploy rejection
'401':
$ref: '#/components/responses/Unauthorized'
description: Invalid or missing API key
'409':
$ref: '#/components/responses/Conflict'
description: A function with this name already exists in the org
'424':
$ref: '#/components/responses/FailedDependency'
description: Function deploy could not be completed; previously deployed code remains live
'429':
$ref: '#/components/responses/RateLimited'
description: Function deploy could not be completed; previously deployed code remains live
'503':
$ref: '#/components/responses/ServiceUnavailable'
description: Function deploy could not be completed; previously deployed code remains live
security:
- BearerAuth: []
parameters:
- name: Idempotency-Key
in: header
required: false
description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
schema:
type: string
minLength: 1
maxLength: 255
/functions/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: getFunction
summary: Get a function
description: 'Returns the full record for a function, including its current
source code and the deploy status / error from the most recent
deploy attempt.
'
tags:
- Functions
responses:
'200':
description: Function record
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: Full function record returned by GET / PUT.
properties:
id:
type: string
format: uuid
name:
type: string
code:
type: string
description: 'The bundled handler source. UTF-8 string up to 1 MiB. The
same value most recently passed as `code` to POST or PUT.
'
deploy_status:
type: string
enum:
- pending
- deployed
- failed
description: "Lifecycle state of the latest deploy attempt:\n * `pending` — deploy in flight; the runtime has not yet\n confirmed the new bundle is live.\n * `deployed` — the running edge handler is the latest code.\n * `failed` — the most recent deploy attempt failed; the\n previously-live code (if any) is still running. The\n `deploy_error` field carries the error message.\n"
deploy_error:
type:
- string
- 'null'
description: 'Error message from the most recent failed deploy, or null
after a successful deploy. Surface this to users to explain
a `failed` status without polling.
'
deployed_at:
type:
- string
- 'null'
format: date-time
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- name
- code
- deploy_status
- created_at
- updated_at
headers:
ratelimit-limit:
description: Maximum number of requests allowed in the current window.
schema:
type: integer
minimum: 1
example: 120
ratelimit-remaining:
description: Remaining requests in the current window.
schema:
type: integer
minimum: 0
example: 118
ratelimit-reset:
description: Unix timestamp (seconds) when the current window resets.
schema:
type: integer
example: 1700000060
ratelimit-policy:
description: Rate-limit policy in `limit;w=seconds` format, e.g. `120;w=60`.
schema:
type: string
example: 120;w=60
'401':
$ref: '#/components/responses/Unauthorized'
description: Invalid or missing API key
'404':
$ref: '#/components/responses/NotFound'
description: Resource not found
security:
- BearerAuth: []
put:
operationId: updateFunction
summary: Update and redeploy a function
description: 'Replaces the function''s source code with the body''s `code` and
triggers a redeploy. Same size limits as `POST /functions`.
Use this verb to push secret writes into the running handler:
passing the same `code` re-runs the deploy and refreshes the
binding set with the latest values from the secrets table.
On deploy failure, the previously-deployed code stays live; the
runtime never serves a half-built bundle. The response uses
`error.code` `deploy_failed`, and the function''s `deploy_error`
field carries the latest deploy error for dashboard/API reads.
'
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
code:
type: string
minLength: 1
maxLength: 1048576
description: New pre-built handler. Same rules as CreateFunctionInput.code. Provide either `code` or `files`, not both.
sourceMap:
type: string
minLength: 1
maxLength: 5242880
files:
type: object
additionalProperties:
type: string
description: 'Source files for a managed build, as a map of path to file
contents. Provide this INSTEAD of `code` to rebuild and
redeploy from source. Same rules as CreateFunctionInput.files.
'
required: []
responses:
'200':
description: Updated function
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: Full function record returned by GET / PUT.
properties:
id:
type: string
format: uuid
name:
type: string
code:
type: string
description: 'The bundled handler source. UTF-8 string up to 1 MiB. The
same value most recently passed as `code` to POST or PUT.
'
deploy_status:
type: string
enum:
- pending
- deployed
- failed
description: "Lifecycle state of the latest deploy attempt:\n * `pending` — deploy in flight; the runtime has not yet\n confirmed the new bundle is live.\n * `deployed` — the running edge handler is the latest code.\n * `failed` — the most recent deploy attempt failed; the\n previously-live code (if any) is still running. The\n `deploy_error` field carries the error message.\n"
deploy_error:
type:
- string
- 'null'
description: 'Error message from the most recent failed deploy, or null
after a successful deploy. Surface this to users to explain
a `failed` status without polling.
'
deployed_at:
type:
- string
- 'null'
format: date-time
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- name
- code
- deploy_status
- created_at
- updated_at
headers:
ratelimit-limit:
description: Maximum number of requests allowed in the current window.
schema:
type: integer
minimum: 1
example: 120
ratelimit-remaining:
description: Remaining requests in the current window.
schema:
type: integer
minimum: 0
example: 118
ratelimit-reset:
description: Unix timestamp (seconds) when the current window resets.
schema:
type: integer
example: 1700000060
ratelimit-policy:
description: Rate-limit policy in `limit;w=seconds` format, e.g. `120;w=60`.
schema:
type: string
example: 120;w=60
'400':
$ref: '#/components/responses/ValidationError'
description: Invalid request parameters or customer-correctable deploy rejection
'401':
$ref: '#/components/responses/Unauthorized'
description: Invalid or missing API key
'404':
$ref: '#/components/responses/NotFound'
description: Resource not found
'424':
$ref: '#/components/responses/FailedDependency'
description: Function deploy could not be completed; previously deployed code remains live
'429':
$ref: '#/components/responses/RateLimited'
description: Function deploy could not be completed; previously deployed code remains live
'503':
$ref: '#/components/responses/ServiceUnavailable'
description: Function deploy could not be completed; previously deployed code remains live
security:
- BearerAuth: []
delete:
operationId: deleteFunction
summary: Delete a function
description: Delete a hosted inbound function. It stops running on future inbound messages.
tags:
- Functions
responses:
'200':
description: Resource deleted
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
const: true
required:
- deleted
headers:
ratelimit-limit:
description: Maximum number of requests allowed in the current window.
schema:
type: integer
minimum: 1
example: 120
ratelimit-remaining:
description: Remaining requests in the current window.
schema:
type: integer
minimum: 0
example: 118
ratelimit-reset:
description: Unix timestamp (seconds) when the current window resets.
schema:
type: integer
example: 1700000060
ratelimit-policy:
description: Rate-limit policy in `limit;w=seconds` format, e.g. `120;w=60`.
schema:
type: string
example: 120;w=60
'401':
$ref: '#/components/responses/Unauthorized'
description: Invalid or missing API key
'404':
$ref: '#/components/responses/NotFound'
description: Resource not found
'502':
$ref: '#/components/responses/BadGateway'
description: Primitive could not complete the downstream SMTP request
security:
- BearerAuth: []
/functions/{id}/test:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: testFunction
summary: Send a test invocation
description: 'Sends a real test email from a Primitive-controlled sender to a
local-part on one of the org''s verified inbound domains. By
default the recipient is a synthetic
`__primitive_function_test+<random>@<domain>` address on a
domain selected to route to the function. Scoped functions use
their scoped domain; fallback functions use a domain that has
no enabled domain-scoped endpoint. Pass `local_part` to
override and exercise routing logic that branches on a specific
recipient (the common pattern when one function handles multiple
inboxes like `summarize@` and `action@`). The function fires
through the normal MX delivery path, so reply / send-mail calls
from inside the handler against the inbound''s `email.id` work
the same as in production. Returns immediately after the send is
queued; the invocation appears on the function''s invocations
list within a few seconds.
Requires that the functio
# --- truncated at 32 KB (129 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/primitive/refs/heads/main/openapi/primitive-functions-api-openapi.yml