openapi: 3.1.0
info:
title: Primitive API
version: 1.0.0
description: |-
Primitive is email infrastructure for AI agents. The Primitive API lets you manage domains, emails, webhook endpoints,
filters, and account settings programmatically.
## Authentication
Most endpoints require a Bearer token in the `Authorization` header:
```
Authorization: Bearer prim_
Authorization: Bearer prim_oat_
```
API keys and OAuth access tokens are org-scoped. Create and manage them in your dashboard
under Settings > API Keys. CLI login plus CLI/agent signup endpoints
explicitly declare `security: []`; they do not require an API key because
they are used to create OAuth CLI sessions.
## Rate Limiting
The API enforces a sliding window rate limit of **120 requests per
60 seconds** per organization. When exceeded, the API returns `429`
with a `Retry-After` header indicating how many seconds to wait.
## Pagination
List endpoints use cursor-based pagination. Responses include a
`meta` object with `total`, `limit`, and `cursor` fields. Pass the
`cursor` value as a query parameter to fetch the next page. When
`cursor` is `null`, there are no more results.
## Response Format
All responses use a consistent envelope:
```json
{
"success": true,
"data": { ... },
"meta": { "total": 42, "limit": 50, "cursor": "..." }
}
```
Errors follow the same pattern:
```json
{
"success": false,
"error": { "code": "not_found", "message": "Email not found" }
}
```
## Webhook signing
Outbound webhook deliveries (configured via the `endpoints` API)
are signed so receivers can verify they came from Primitive and
have not been tampered with in transit. The signing scheme is
deliberately simple so it can be reimplemented in any language
in a few lines. The Node SDK's `verifyWebhookSignature` helper
is the reference implementation; the wire details below let you
write a verifier in Python, Go, Ruby, etc. without reading our
source.
**Header**: `Primitive-Signature: t=,v1=`
A legacy `MyMX-Signature` header is also sent on every delivery
with the same value, retained for back-compatibility with
integrations written before the rename. New code should read
`Primitive-Signature`.
**Signed string**: `${timestamp}.${rawBody}` where `timestamp`
is the Unix-seconds integer from the `t=` parameter and
`rawBody` is the exact bytes of the HTTP request body BEFORE
any JSON decoding. Verify against the raw body, not a
re-serialized parse, or you will silently mismatch on
insignificant whitespace.
**Signature**: HMAC-SHA256 of the signed string, hex-encoded
(lowercase). Use the account's webhook secret as the HMAC key,
as a UTF-8 byte sequence.
**Secret**: returned by `GET /account/webhook-secret`. The
string looks base64-shaped (e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`)
but is NOT base64; use it AS-IS as a UTF-8 string for the HMAC
key. Base64-decoding before HMAC will silently produce
mismatched signatures.
**Tolerance**: by convention, reject deliveries whose `t=`
timestamp is more than 5 minutes off your wall-clock to defend
against replay attacks. The Node SDK's helper enforces this by
default.
**Verification recipe** (any language):
```
1. Read the raw HTTP body (do not parse).
2. Read `Primitive-Signature: t=,v1=`.
3. Reject if abs(now - ts) > 300 seconds.
4. expected = HMAC_SHA256_hex(secret_utf8, f"{ts}.{rawBody}")
5. Constant-time compare expected to sig. Reject if not equal.
```
For Node, use `verifyWebhookSignature` from
`@primitivedotdev/sdk/webhook` (or the higher-level
`handleWebhook` helper if you want a one-liner). For other
languages, the recipe above is everything you need.
Test deliveries: `POST /endpoints/{id}/test` triggers a fake
delivery to your endpoint URL, signed with your real account
secret, so you can confirm verification end-to-end without
needing real inbound mail. The test response carries the exact
`signature` header value sent on the wire so you can compare
strings directly.
## Errors
Every 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.
## Authorization and roles
Access 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.
## Versioning
The 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.
Breaking 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: CLI
description: Browser-assisted CLI authentication
- name: Agent
description: Agent signup and authentication
- name: Account
description: Manage your account settings, storage, and webhook secret
- name: Domains
description: Claim, verify, and manage email domains
- name: Inbox
description: Check inbound email setup and processing readiness
- name: Emails
description: List, inspect, and manage received emails
- name: Search
description: Semantic and hybrid search across received and sent mail
- name: Sending
description: Send outbound emails through the Primitive API
- name: Threads
description: Conversation threads spanning received and sent emails
- name: Endpoints
description: Manage webhook endpoints that receive email events
- name: Filters
description: Manage whitelist and blocklist filter rules
- name: Routes
description: |
Recipient routing: route inbound mail to a single destination per recipient
address. Rules bind an address pattern (exact or wildcard) to an endpoint;
`function_id` routes an address to a function, minting its route-target
endpoint.
- name: Payments
description: |
Collect and pay stablecoin (USDC) payments with x402. Settlement is
non-custodial: funds move directly from payer to payee on-chain via an
EIP-3009 authorization the payer signs with their own key, and Primitive
never holds funds. The payee registers a payout address and creates a
challenge; the payer signs and settles it under a configurable spend
policy (kill-switch, per-payment and per-day caps, payee allowlist).
- name: Wake
description: |
Wake scheduling: schedule and send typed wake commands to your own
functions over real DKIM-signed email on a cron cadence, and manage the
per-target allowlist that authorizes which senders may wake a function.
- name: Webhook Deliveries
description: View and replay webhook delivery attempts
- 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.
- name: Memories
description: |
Durable org-scoped or function-scoped JSON key-value storage for
agents and functions. Keys are caller-defined. Function scope is
always addressed by the function id UUID, not by function name.
- name: Registries
description: |
The Agent Registry: ownable directories of agents, addressable by a
registry-scoped handle. A registry's publish policy (owner_only, request,
or open) decides whether a publish lists immediately or pends owner
approval. An agent is defined once with a globally unique,
reachability-verified address, then published into any registry under a
handle. Discovery reads (list, resolve, get) are public for public
registries; managing a registry and moderating requests use the owner's
API key.
- name: Templates
description: Public Function template registry reads used to browse installable agent templates.
- name: Demo
description: Public, no-account sandbox operations that mirror authenticated endpoints with synthetic data so an agent can learn the request and response shapes before signing up.
- name: Discovery
description: Unauthenticated entry point that lists the API base URL, how to obtain credentials, and the operations callable without a token.
- name: Service
description: Operational endpoints such as the unauthenticated health/liveness probe.
paths:
/cli/login/start:
post:
operationId: startCliLogin
summary: Start CLI browser login
description: |
Starts a browser-assisted CLI login session. The response includes a
device code for polling and a user code that the user approves in the
browser. This endpoint does not require an API key.
tags:
- CLI
security: []
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
device_name:
type: string
minLength: 1
maxLength: 80
description: Human-readable device name shown during browser approval
metadata:
type: object
additionalProperties: true
description: Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer
responses:
"201":
description: CLI login session created
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
device_code:
type: string
description: Opaque code used by the CLI to poll for approval
user_code:
type: string
pattern: ^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$
description: Short code the user confirms in the browser
verification_uri:
type: string
description: Browser URL where the user approves the login
verification_uri_complete:
type: string
description: Browser URL with the user code prefilled
expires_in:
type: integer
description: Seconds until the login session expires
interval:
type: integer
description: Minimum seconds between poll requests
required:
- device_code
- user_code
- verification_uri
- verification_uri_complete
- expires_in
- interval
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/cli/login/poll:
post:
operationId: pollCliLogin
summary: Poll CLI browser login
description: |
Polls a CLI login session until the browser approval either succeeds,
is denied, expires, or is polled too quickly. The OAuth token set is
created only after approval and is returned exactly once.
tags:
- CLI
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
device_code:
type: string
minLength: 1
required:
- device_code
responses:
"200":
description: CLI login approved and OAuth token set created
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
api_key:
type: string
description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
key_id:
type: string
format: uuid
description: Legacy alias for oauth_grant_id
key_prefix:
type: string
description: Legacy display prefix derived from access_token
access_token:
type: string
description: OAuth access token for CLI API authentication
refresh_token:
type: string
description: OAuth refresh token used by the CLI to renew access
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until access_token expires
auth_method:
type: string
enum:
- oauth
oauth_grant_id:
type: string
format: uuid
oauth_client_id:
type: string
org_id:
type: string
format: uuid
org_name:
type:
- string
- "null"
required:
- api_key
- key_id
- key_prefix
- access_token
- refresh_token
- token_type
- expires_in
- auth_method
- oauth_grant_id
- oauth_client_id
- org_id
- org_name
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request, pending authorization, slow polling, expired token, or invalid device code
"403":
$ref: "#/components/responses/Forbidden"
description: CLI login was denied in the browser
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
/cli/signup/start:
post:
operationId: startCliSignup
summary: Start CLI account signup
description: |
Starts a terminal-native CLI signup. `signup_code` is optional;
omit it to sign up without one. The API creates a pending signup
session, sends an email verification code, and returns an opaque
signup token used by the resend and verify steps. This endpoint
does not require an API key.
tags:
- CLI
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
maxLength: 254
signup_code:
type: string
minLength: 1
maxLength: 128
description: Optional signup code. Omit if you do not have one.
terms_accepted:
type: boolean
const: true
description: Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy
device_name:
type: string
minLength: 1
maxLength: 80
description: Human-readable device name used for the created CLI OAuth grant
metadata:
type: object
additionalProperties: true
description: Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer
required:
- email
- terms_accepted
responses:
"201":
description: CLI signup session created and verification email sent
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
signup_token:
type: string
description: Opaque token used to verify or resend the pending CLI signup
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- signup_token
- email
- expires_in
- resend_after
- verification_code_length
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/cli/signup/resend:
post:
operationId: resendCliSignupVerification
summary: Resend CLI signup verification code
description: |
Sends a new email verification code for a pending CLI signup session.
This endpoint does not require an API key.
tags:
- CLI
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
required:
- signup_token
responses:
"200":
description: Verification email resent
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- email
- expires_in
- resend_after
- verification_code_length
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid token or expired token
"429":
$ref: "#/components/responses/RateLimited"
description: Global rate limit exceeded or resend requested too quickly
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
/cli/signup/verify:
post:
operationId: verifyCliSignup
summary: Verify CLI signup and create OAuth session
description: |
Verifies the email code for a CLI signup session and creates the
account. When the session was started with a `signup_code`, the
reserved code is redeemed; sessions started without a code skip
the redemption step. Either way an org-scoped OAuth CLI session
is created and the token set is returned exactly once. This
endpoint does not require an API key.
tags:
- CLI
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
verification_code:
type: string
minLength: 1
maxLength: 32
password:
type: string
minLength: 1
maxLength: 1024
required:
- signup_token
- verification_code
responses:
"200":
description: CLI signup verified and OAuth token set created
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
api_key:
type: string
description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
key_id:
type: string
format: uuid
description: Legacy alias for oauth_grant_id
key_prefix:
type: string
description: Legacy display prefix derived from access_token
access_token:
type: string
description: OAuth access token for CLI API authentication
refresh_token:
type: string
description: OAuth refresh token used by the CLI to renew access
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until access_token expires
auth_method:
type: string
enum:
- oauth
oauth_grant_id:
type: string
format: uuid
oauth_client_id:
type: string
org_id:
type: string
format: uuid
org_name:
type:
- string
- "null"
required:
- api_key
- key_id
- key_prefix
- access_token
- refresh_token
- token_type
- expires_in
- auth_method
- oauth_grant_id
- oauth_client_id
- org_id
- org_name
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request, invalid verification code, expired token, invalid signup code, rejected password, or account creation failure
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/agent/signup/start:
post:
operationId: startAgentSignup
summary: Start agent account signup
description: |
Starts an agent-native signup session. `signup_code` is optional;
omit it to sign up without one. The API creates a pending signup
session, sends an email verification code, and returns an opaque
signup token used by the resend and verify steps. This endpoint
does not require an API key.
tags:
- Agent
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
maxLength: 254
signup_code:
type: string
minLength: 1
maxLength: 128
description: Optional signup code. Omit if you do not have one.
terms_accepted:
type: boolean
const: true
description: Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy
device_name:
type: string
minLength: 1
maxLength: 80
description: Human-readable device name used for the created agent OAuth session
metadata:
type: object
additionalProperties: true
description: Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer
required:
- email
- terms_accepted
responses:
"201":
description: Agent signup session created and verification email sent
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
signup_token:
type: string
description: Opaque token used to verify or resend the pending agent signup
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- signup_token
- email
- expires_in
- resend_after
- verification_code_length
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/agent/signup/resend:
post:
operationId: resendAgentSignupVerification
summary: Resend agent signup verification code
description: |
Sends a new email verification code for a pending agent signup session.
This endpoint does not require an API key.
tags:
- Agent
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
required:
- signup_token
responses:
"200":
description: Verification email resent
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- email
- expires_in
- resend_after
- verification_code_length
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid token or expired token
"429":
$ref: "#/components/responses/RateLimited"
description: Global rate limit exceeded or resend requested too quickly
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
/agent/signup/verify:
post:
operationId: verifyAgentSignup
summary: Verify agent signup and create OAuth tokens
description: |
Verifies the email code for an agent signup session and creates
the account when needed. When the session was started with a
`signup_code`, the reserved code is redeemed; sessions started
without a code skip the redemption step. An org-scoped OAuth
session for CLI authentication is minted and the raw tokens are
returned exactly once. For existing users, the optional `org_id`
selects which accessible workspace should receive the new
session (no signup-code redemption is performed for existing
users regardless of how the session was started).
tags:
- Agent
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
verification_code:
type: string
minLength: 1
maxLength: 32
org_id:
type: string
format: uuid
description: Optional workspace id to target when the verified email already belongs to multiple workspaces
required:
- signup_token
- verification_code
responses:
"200":
description: Agent signup verified and OAuth tokens created
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
api_key:
type: string
description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
key_id:
type: string
format: uuid
description: Legacy alias for oauth_grant_id
key_prefix:
type: string
description: Legacy display prefix derived from access_token
access_token:
type: string
description: OAuth access token for CLI API authentication
refresh_token:
type: string
description: OAuth refresh token used by the CLI to renew access
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until access_token expires
auth_method:
type: string
enum:
- oauth
oauth_grant_id:
type: string
format: uuid
oauth_client_id:
type: string
org_id:
type: string
format: uuid
org_name:
type:
- string
- "null"
orgs:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
name:
type:
- string
- "null"
required:
- id
- name
description: Workspaces available to the verified email. The minted session targets `org_id`.
required:
- api_key
- key_id
- key_prefix
- access_token
- refresh_token
- token_type
- expires_in
- auth_method
- oauth_grant_id
- oauth_client_id
- org_id
- org_name
- orgs
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request, invalid verification code, expired token, invalid signup code, or account creation failure
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"409":
$ref: "#/components/responses/Conflict"
description: Existing account is not in a usable workspace state
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/agent/accounts:
post:
operationId: createAgentAccount
summary: Create an emailless agent account
description: |
Creates an emailless agent account without authentication and returns a
one-time API key (prefixed `prim_`) plus a provisioned managed inbox.
The account is on the `agent` plan: reply-only (it can send only to
addresses that have already sent it authenticated mail) with tight send
limits. Use the returned `api_key` as a Bearer token on later calls. The
account can be upgraded to a full developer account by confirming an
email through the claim flow. This endpoint does not require an API key.
tags:
- Agent
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
terms_accepted:
type: boolean
enum:
- true
description: Must be true to accept the Terms of Service and Privacy Policy.
device_name:
type: string
minLength: 1
maxLength: 80
description: Optional label for the device or agent creating the account.
required:
- terms_accepted
responses:
"200":
description: Agent account created; the API key is returned once
headers:
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
api_key:
type: string
description: One-time API key (prefixed `prim_`). Shown once; store it securely.
org_id:
type: string
format: uuid
address:
type:
- string
- "null"
description: Provisioned managed inbox FQDN, or null if the inbox publish was deferred.
plan:
type: string
enum:
- agent
limits:
type: object
description: Plan-derived quota limits for an account.
properties:
storage_mb:
type: number
send_per_hour:
type: number
send_per_day:
type: number
api_per_minute:
type: number
webhooks_max_global:
type:
- number
- "null"
webhooks_per_domain:
type: boolean
filters_per_domain:
type: boolean
spam_thresholds_per_domain:
type: boolean
required:
- storage_mb
- send_per_hour
- send_per_day
- api_per_minute
- webhooks_max_global
- webhooks_per_domain
- filters_per_domain
- spam_thresholds_per_domain
upgrade:
type: object
description: In-band pointer to the upgrade path for an agent account.
properties:
plan:
type: string
enum:
- developer
description:
type: string
claim_path:
type: string
required:
- plan
- description
- claim_path
required:
- api_key
- org_id
- address
- plan
- limits
- upgrade
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/agent/claim/start:
post:
operationId: startAgentClaim
summary: Start an agent account email claim
description: |
Begins upgrading an emailless `agent` account into a full `developer`
account by confirming an email address. Authenticated by the agent's own
API key (the org is taken from the credential). Sends a verification
code to the supplied email and returns the claim session id plus resend
timing. Submit the code to `/agent/claim/verify` to complete the
upgrade. Confirming an email that already belongs to a Primitive account
is rejected.
tags:
- Agent
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
maxLength: 254
description: Email to confirm. Must not already belong to a Primitive account.
required:
- email
responses:
"200":
description: Claim started and verification email sent
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
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
claim_session_id:
type: string
resend_after_seconds:
type: integer
expires_in_seconds:
type: integer
required:
- claim_session_id
- resend_after_seconds
- expires_in_seconds
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The email is already in use, or the account is not claimable
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/agent/claim/verify:
post:
operationId: verifyAgentClaim
summary: Verify an agent account email claim
description: |
Confirms the verification code emailed by `/agent/claim/start` and
upgrades the account to the `developer` plan. The org id, API key, and
managed inbox all carry over; the send cap lifts. Authenticated by the
agent's own API key.
tags:
- Agent
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
verification_code:
type: string
minLength: 1
maxLength: 32
description: The verification code emailed by the claim start step.
required:
- verification_code
responses:
"200":
description: Claim verified; account upgraded to developer
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
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
org_id:
type: string
format: uuid
plan:
type: string
enum:
- developer
email:
type: string
format: email
limits:
type: object
description: Plan-derived quota limits for an account.
properties:
storage_mb:
type: number
send_per_hour:
type: number
send_per_day:
type: number
api_per_minute:
type: number
webhooks_max_global:
type:
- number
- "null"
webhooks_per_domain:
type: boolean
filters_per_domain:
type: boolean
spam_thresholds_per_domain:
type: boolean
required:
- storage_mb
- send_per_hour
- send_per_day
- api_per_minute
- webhooks_max_global
- webhooks_per_domain
- filters_per_domain
- spam_thresholds_per_domain
required:
- org_id
- plan
- email
- limits
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The account is already claimed, or the email is in use
"410":
$ref: "#/components/responses/Gone"
description: The claim or its verification code has expired
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/agent/claim/link:
post:
operationId: createAgentClaimLink
summary: Create a browser claim link
description: |
Mints an opaque, single-use link an agent can hand to a human to
complete the email-confirmation upgrade in a browser. Authenticated by
the agent's own API key. `claim_url` is null when the API host cannot
resolve a web origin to build the link.
tags:
- Agent
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
description: No fields; an empty object is accepted.
properties: {}
responses:
"200":
description: Claim link created
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
Cache-Control:
schema:
type: string
description: Always `no-store`
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
claim_token:
type: string
claim_url:
type:
- string
- "null"
description: Browser URL to hand to a human, or null if no web origin is configured.
expires_in_seconds:
type: integer
required:
- claim_token
- claim_url
- expires_in_seconds
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The account is not claimable (not an agent account, or already claimed)
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/cli/logout:
post:
operationId: cliLogout
summary: Revoke the current CLI OAuth session
description: |
Revokes the OAuth grant used to authenticate the request. API-key
authenticated legacy logout requests succeed without deleting server API
keys so old local CLI state can be cleared safely.
tags:
- CLI
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
key_id:
type: string
format: uuid
description: Optional id guard; when provided it must match the authenticated OAuth grant id or API key id
responses:
"200":
description: CLI logout completed
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
revoked:
type: boolean
description: True when an OAuth grant was revoked. False for API-key-authenticated legacy logout, which only clears local CLI state.
key_id:
type: string
format: uuid
description: API key id for API-key-authenticated legacy logout
oauth_grant_id:
type: string
format: uuid
description: OAuth grant id revoked by OAuth-authenticated logout
required:
- revoked
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
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
/account:
get:
operationId: getAccount
summary: Get account info
tags:
- Account
responses:
"200":
description: Account details
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
plan:
type: string
limits:
type: object
description: Plan-derived quota limits for an account.
properties:
storage_mb:
type: number
send_per_hour:
type: number
send_per_day:
type: number
api_per_minute:
type: number
webhooks_max_global:
type:
- number
- "null"
webhooks_per_domain:
type: boolean
filters_per_domain:
type: boolean
spam_thresholds_per_domain:
type: boolean
required:
- storage_mb
- send_per_hour
- send_per_day
- api_per_minute
- webhooks_max_global
- webhooks_per_domain
- filters_per_domain
- spam_thresholds_per_domain
entitlements:
type: array
items:
type: string
description: |
Granted org entitlement keys (sorted). A headless caller reads its
capabilities here â e.g. an emailless agent seeing only
["send_mail", "send_to_known_addresses"] knows it is reply-only.
managed_inbox_address:
type:
- string
- "null"
description: The managed inbox FQDN to reply as, or null if the org has no managed inbox.
created_at:
type: string
format: date-time
onboarding_completed:
type: boolean
onboarding_step:
type:
- string
- "null"
stripe_subscription_status:
type:
- string
- "null"
subscription_current_period_end:
type:
- string
- "null"
format: date-time
subscription_cancel_at_period_end:
type:
- boolean
- "null"
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
discard_content_on_webhook_confirmed:
type: boolean
webhook_secret_rotated_at:
type:
- string
- "null"
format: date-time
required:
- id
- email
- plan
- limits
- entitlements
- managed_inbox_address
- created_at
- discard_content_on_webhook_confirmed
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: []
description: "Retrieve the authenticated organization's account: organization identity, current plan, usage, and onboarding state."
patch:
operationId: updateAccount
summary: Update account settings
tags:
- Account
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
description: Global spam score threshold (0-15). Emails scoring above this are rejected. Set to null to disable.
discard_content_on_webhook_confirmed:
type: boolean
description: Whether to discard email content after the webhook endpoint confirms receipt.
minProperties: 1
responses:
"200":
description: Updated account
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
plan:
type: string
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
discard_content_on_webhook_confirmed:
type: boolean
required:
- id
- email
- plan
- discard_content_on_webhook_confirmed
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
description: Update the mutable account settings (such as display name and notification preferences) for the authenticated organization.
/account/storage:
get:
operationId: getStorageStats
summary: Get storage usage
tags:
- Account
responses:
"200":
description: Storage statistics
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
used_bytes:
type: integer
description: Total storage used in bytes
used_kb:
type: number
description: Total storage used in kilobytes (1 decimal)
used_mb:
type: number
description: Total storage used in megabytes (2 decimals)
quota_mb:
type: number
description: Storage quota in megabytes (based on plan)
percentage:
type: number
description: Percentage of quota used (1 decimal)
emails_count:
type: integer
description: Number of stored emails
required:
- used_bytes
- used_kb
- used_mb
- quota_mb
- percentage
- emails_count
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: []
description: Return stored-email storage usage and the remaining quota for the authenticated organization.
/account/webhook-secret:
get:
operationId: getWebhookSecret
summary: Get webhook signing secret
description: |
Returns the webhook signing secret for your account. If no
secret exists yet, one is generated automatically on first
access.
Signing is account-scoped, not per-endpoint. Every webhook
delivery from any of your registered endpoints is signed
with this single secret. Rotate via
`POST /account/webhook-secret/rotate`.
**Secret format**: the returned string looks base64-shaped
(e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`) but is NOT base64.
Use it AS-IS as a UTF-8 string when computing HMAC over a
delivery body. Base64-decoding before HMAC will silently
produce mismatched signatures.
See the API-level "Webhook signing" section for the full
wire format (header name, signed string shape, hash algo,
tolerance) including a language-agnostic verification
recipe.
tags:
- Account
responses:
"200":
description: Webhook secret
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
secret:
type: string
description: The webhook signing secret value
required:
- secret
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: []
/account/webhook-secret/rotate:
post:
operationId: rotateWebhookSecret
summary: Rotate webhook signing secret
description: |
Generates a new webhook signing secret, replacing the current one.
Rate limited to once per 60 minutes.
tags:
- Account
responses:
"200":
description: New webhook secret
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
secret:
type: string
description: The webhook signing secret value
required:
- secret
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/domains:
get:
operationId: listDomains
summary: List all domains
description: |
Returns all verified and unverified domains for your organization,
sorted by creation date (newest first). Each domain includes a
`verified` boolean to distinguish between the two states.
tags:
- Domains
responses:
"200":
description: List of domains
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: array
items:
description: |
A domain can be either verified or unverified. Verified domains have
`is_active` and `spam_threshold` fields. Unverified domains have a
`verification_token` and `dns_records` for DNS setup.
oneOf:
- type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain:
type: string
verified:
type: boolean
const: true
is_active:
type: boolean
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
verification_token:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- org_id
- domain
- verified
- is_active
- created_at
- type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain:
type: string
verified:
type: boolean
const: false
verification_token:
type: string
description: Add this value as a TXT record to verify ownership
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- MX
- TXT
description: DNS record type.
name:
type: string
description: DNS-provider host/name value relative to the managed root zone.
fqdn:
type: string
description: Fully-qualified DNS record name.
value:
type: string
description: Exact value to publish.
priority:
type: integer
description: MX priority. Present only for MX records.
ttl:
type: integer
description: Suggested TTL in seconds when the API can provide one.
required:
type: boolean
const: true
purpose:
type: string
enum:
- inbound_mx
- ownership_verification
- spf
- dkim
- dmarc
- tls_reporting
status:
type: string
enum:
- pending
- found
- missing
- incorrect
message:
type: string
description: Short explanation of why this record is needed.
required:
- type
- name
- fqdn
- value
- required
- purpose
- status
created_at:
type: string
format: date-time
required:
- id
- org_id
- domain
- verified
- verification_token
- created_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: addDomain
summary: Claim a new domain
description: |
Creates an unverified domain claim and returns the exact
DNS records to publish in `dns_records`. Publish those
records before calling the verify endpoint. To give users
an importable DNS file, call `downloadDomainZoneFile` or run
`primitive domains zone-file --id `.
tags:
- Domains
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
domain:
type: string
minLength: 1
maxLength: 253
description: The domain name to claim (e.g. "example.com")
confirmed:
type: boolean
description: Set to true to confirm replacing an existing mailbox provider after an mx_conflict response.
outbound:
type: boolean
deprecated: true
description: Deprecated and ignored. Outbound DNS is provisioned for every new domain claim.
required:
- domain
responses:
"201":
description: Domain claim created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain:
type: string
verified:
type: boolean
const: false
verification_token:
type: string
description: Add this value as a TXT record to verify ownership
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- MX
- TXT
description: DNS record type.
name:
type: string
description: DNS-provider host/name value relative to the managed root zone.
fqdn:
type: string
description: Fully-qualified DNS record name.
value:
type: string
description: Exact value to publish.
priority:
type: integer
description: MX priority. Present only for MX records.
ttl:
type: integer
description: Suggested TTL in seconds when the API can provide one.
required:
type: boolean
const: true
purpose:
type: string
enum:
- inbound_mx
- ownership_verification
- spf
- dkim
- dmarc
- tls_reporting
status:
type: string
enum:
- pending
- found
- missing
- incorrect
message:
type: string
description: Short explanation of why this record is needed.
required:
- type
- name
- fqdn
- value
- required
- purpose
- status
created_at:
type: string
format: date-time
required:
- id
- org_id
- domain
- verified
- verification_token
- created_at
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"409":
$ref: "#/components/responses/Conflict"
description: |
Domain claim conflicts with existing state. Two error codes
are possible:
* `mx_conflict`: the domain's current MX records point at
another mailbox provider. The response includes
`error.details.mx_conflict` with the detected provider
and a suggested subdomain.
* `conflict`: the domain is already claimed by another
org, or a pending claim exists for another user.
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
/domains/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
patch:
operationId: updateDomain
summary: Update domain settings
description: |
Update a verified domain's settings. Only verified domains can be
updated. Per-domain spam thresholds require a Pro plan.
tags:
- Domains
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
is_active:
type: boolean
description: Whether the domain accepts incoming emails
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
description: Per-domain spam threshold override (Pro plan required)
minProperties: 1
responses:
"200":
description: Updated domain
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain:
type: string
verified:
type: boolean
const: true
is_active:
type: boolean
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
verification_token:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- org_id
- domain
- verified
- is_active
- created_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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteDomain
summary: Delete a domain
description: Remove a domain from the organization. Inbound mail for its addresses stops being accepted.
tags:
- Domains
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
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/domains/{id}/verify:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: verifyDomain
summary: Verify domain ownership
description: |
Checks DNS records required for inbound routing, ownership,
and outbound authentication: MX, ownership TXT, SPF, DKIM,
DMARC, and TLS-RPT.
On success, the domain is promoted from unverified to verified.
On failure, returns which checks passed and which failed,
plus the exact DNS records still expected. To give users
an importable DNS file for missing records, call
`downloadDomainZoneFile` or run
`primitive domains zone-file --id `.
tags:
- Domains
responses:
"200":
description: Verification result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
oneOf:
- type: object
properties:
verified:
type: boolean
const: true
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- MX
- TXT
description: DNS record type.
name:
type: string
description: DNS-provider host/name value relative to the managed root zone.
fqdn:
type: string
description: Fully-qualified DNS record name.
value:
type: string
description: Exact value to publish.
priority:
type: integer
description: MX priority. Present only for MX records.
ttl:
type: integer
description: Suggested TTL in seconds when the API can provide one.
required:
type: boolean
const: true
purpose:
type: string
enum:
- inbound_mx
- ownership_verification
- spf
- dkim
- dmarc
- tls_reporting
status:
type: string
enum:
- pending
- found
- missing
- incorrect
message:
type: string
description: Short explanation of why this record is needed.
required:
- type
- name
- fqdn
- value
- required
- purpose
- status
required:
- verified
- type: object
properties:
verified:
type: boolean
const: false
mxFound:
type: boolean
description: Whether MX records point to Primitive
txtFound:
type: boolean
description: Whether the TXT verification record was found
spfFound:
type: boolean
description: Whether the SPF record includes Primitive.
dkimFound:
type: boolean
description: Whether the DKIM public key record was found.
dmarcFound:
type: boolean
description: Whether the DMARC record was found.
tlsRptFound:
type: boolean
description: Whether the TLS-RPT record was found.
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- MX
- TXT
description: DNS record type.
name:
type: string
description: DNS-provider host/name value relative to the managed root zone.
fqdn:
type: string
description: Fully-qualified DNS record name.
value:
type: string
description: Exact value to publish.
priority:
type: integer
description: MX priority. Present only for MX records.
ttl:
type: integer
description: Suggested TTL in seconds when the API can provide one.
required:
type: boolean
const: true
purpose:
type: string
enum:
- inbound_mx
- ownership_verification
- spf
- dkim
- dmarc
- tls_reporting
status:
type: string
enum:
- pending
- found
- missing
- incorrect
message:
type: string
description: Short explanation of why this record is needed.
required:
- type
- name
- fqdn
- value
- required
- purpose
- status
error:
type: string
description: Human-readable verification failure reason
required:
- verified
- mxFound
- txtFound
- error
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
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
/domains/{id}/zone-file:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: downloadDomainZoneFile
summary: Download domain DNS zone file
description: Downloads a BIND-format DNS zone file containing the DNS records required for a domain claim. Agents should offer this after `addDomain` when users want to import DNS records instead of copying each record manually.
tags:
- Domains
security:
- BearerAuth: []
parameters:
- name: outbound_only
in: query
schema:
type: boolean
description: When true, include only outbound DNS records. Verified domains default to outbound-only; pending claims default to all required records.
responses:
"200":
description: BIND-format zone file
content:
text/plain:
schema:
type: string
format: binary
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
Content-Disposition:
schema:
type: string
example: attachment; filename="example.com.zone"
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/inbox/status:
get:
operationId: getInboxStatus
summary: Get inbound inbox readiness
description: Returns one consolidated view of domain verification, webhook/function processing routes, deployed functions, and recent inbound mail. Agents should use this before guiding users through inbound email setup.
tags:
- Inbox
security:
- BearerAuth: []
responses:
"200":
description: Consolidated inbox readiness status
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
ready:
type: boolean
description: True when an active inbound domain and at least one processing route are both ready.
receiving_ready:
type: boolean
description: True when at least one active verified or managed domain can receive mail.
processing_ready:
type: boolean
description: True when at least one receiving-ready domain has an enabled webhook or function route.
summary:
type: string
next_actions:
type: array
items:
type: object
additionalProperties: false
properties:
kind:
type: string
enum:
- add_domain
- verify_domain
- configure_processing
- send_test_email
- fix_failed_functions
message:
type: string
description: Human-readable next step.
command:
type: string
description: Suggested Primitive CLI command when there is an obvious next step.
required:
- kind
- message
domains:
type: array
items:
type: object
additionalProperties: false
properties:
id:
type: string
domain:
type: string
verified:
type: boolean
active:
type: boolean
managed:
type: boolean
receiving_ready:
type: boolean
processing_ready:
type: boolean
processing_route_count:
type: integer
endpoint_count:
type: integer
enabled_endpoint_count:
type: integer
function_endpoint_count:
type: integer
email_count:
type: integer
description: Number of inbound emails received for this domain in the last 30 days.
latest_email_received_at:
type:
- string
- "null"
format: date-time
description: Most recent inbound email received for this domain in the last 30 days.
status:
type: string
enum:
- ready
- stored_only
- pending_dns
- inactive
required:
- id
- domain
- verified
- active
- managed
- receiving_ready
- processing_ready
- processing_route_count
- endpoint_count
- enabled_endpoint_count
- function_endpoint_count
- email_count
- latest_email_received_at
- status
endpoints:
type: object
additionalProperties: false
properties:
total:
type: integer
enabled:
type: integer
disabled:
type: integer
fallback_enabled:
type: integer
domain_scoped_enabled:
type: integer
http_enabled:
type: integer
function_enabled:
type: integer
required:
- total
- enabled
- disabled
- fallback_enabled
- domain_scoped_enabled
- http_enabled
- function_enabled
functions:
type: object
additionalProperties: false
properties:
total:
type: integer
deployed:
type: integer
pending:
type: integer
failed:
type: integer
required:
- total
- deployed
- pending
- failed
recent_emails:
type: object
description: Inbound email activity from the last 30 days.
additionalProperties: false
properties:
total:
type: integer
description: Number of inbound emails received in the last 30 days.
latest_received_at:
type:
- string
- "null"
format: date-time
description: Most recent inbound email received in the last 30 days.
required:
- total
- latest_received_at
required:
- ready
- receiving_ready
- processing_ready
- summary
- next_actions
- domains
- endpoints
- functions
- recent_emails
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
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/emails:
get:
operationId: listEmails
summary: List inbound emails
description: |
Returns a paginated list of INBOUND emails received at your
verified domains. Outbound messages sent via /send-mail are
not included; this endpoint is the inbox view, not a
unified send/receive history.
Supports filtering by domain, status, date range, and
free-text search across subject, sender, and recipient
fields.
For a compact text-table summary of the most recent N
inbounds (no filters, no cursor pagination), the CLI ships
`primitive emails:latest` as a one-line-per-email shortcut.
It's TTY-aware so id columns are full UUIDs when piped, and
a `--json` flag returns the same envelope this endpoint
does. Use whichever fits the call site.
tags:
- Emails
parameters:
- name: cursor
in: query
schema:
type: string
description: |
Pagination cursor from a previous response's `meta.cursor` field.
Format: `{ISO-datetime}|{id}`
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
description: Number of results per page
- name: domain_id
in: query
schema:
type: string
format: uuid
description: Filter by domain ID
- name: status
in: query
schema:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
description: |
Filter inbound rows by lifecycle status. See `EmailStatus`
for what each value means. Note that the webhook delivery
state is a SEPARATE lifecycle on the same row; filter by
`webhook_status` semantics is not currently supported on
this endpoint.
- name: search
in: query
schema:
type: string
maxLength: 500
description: Search subject, sender, and recipient (case-insensitive)
- name: date_from
in: query
schema:
type: string
format: date-time
description: Filter emails created on or after this timestamp
- name: date_to
in: query
schema:
type: string
format: date-time
description: Filter emails created on or before this timestamp
- name: since
in: query
schema:
type: string
maxLength: 200
description: |
Forward-tail cursor. Returns rows that became visible AFTER this
cursor, oldest-first, so a caller can stream new inbound mail by
re-passing the cursor from each response. Mutually exclusive with
`cursor` (which pages history newest-first). Pass the `meta.cursor`
from the previous `since` response; an empty page means caught up.
- name: wait
in: query
schema:
type: integer
minimum: 0
maximum: 30
description: |
Long-poll: hold the request up to this many seconds waiting for new
mail past `since`, returning as soon as any arrives (or an empty
page when the wait elapses). Requires `since`. Omitted means no wait
(returns immediately); the server treats an absent value as 0. NOT
given an OpenAPI `default` on purpose: a default makes some
generators (e.g. openapi-python-client) send `wait=0` on every call,
which then fails the `wait` requires `since` check for plain history
listings.
responses:
"200":
description: Paginated list of emails
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
meta:
type: object
properties:
total:
type: integer
description: Total number of matching records
limit:
type: integer
description: Page size used for this request
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if no more results
required:
- total
- limit
- cursor
required:
- success
- data
- meta
- type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
status:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. For most legitimate mail this equals the bare
address in the From header; for mailing lists, bounce
handlers, and forwarders it is typically the bounce address
rather than the human-visible sender.
For the parsed From-header value (with display name handling
and a sender-fallback when the header is unparseable), GET
the email by id and use `from_email`.
recipient:
type: string
subject:
type:
- string
- "null"
domain:
type: string
spam_score:
type:
- number
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
raw_size_bytes:
type:
- integer
- "null"
webhook_status:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
webhook_attempt_count:
type: integer
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Fetch
`/threads/{thread_id}` for the full ordered thread. NULL on
messages received before threading was enabled.
required:
- id
- status
- sender
- recipient
- domain
- created_at
- received_at
- webhook_attempt_count
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
security:
- BearerAuth: []
/emails/search:
get:
operationId: searchEmails
summary: Search inbound emails
description: |
Searches inbound emails with structured filters and optional
full-text matching across parsed email fields. This endpoint is
optimized for filtered inbox views and CLI polling workflows:
callers that only need new accepted mail can pass
`sort=received_at_asc`, `snippet=false`, `include_facets=false`,
and a `date_from` timestamp.
`q`, `subject`, and `body` use the same English full-text index
as the web inbox search. Structured filters such as `from`, `to`,
`domain_id`, status, attachment presence, and spam score bounds
are combined with the text query.
tags:
- Emails
parameters:
- name: q
in: query
schema:
type: string
maxLength: 500
description: Full-text search DSL query.
- name: from
in: query
schema:
type: string
maxLength: 255
description: Filter by sender address or sender domain.
- name: to
in: query
schema:
type: string
maxLength: 255
description: Filter by recipient address or recipient domain.
- name: subject
in: query
schema:
type: string
maxLength: 500
description: Full-text search restricted to the subject field.
- name: body
in: query
schema:
type: string
maxLength: 2000
description: Full-text search restricted to the parsed text body.
- name: domain_id
in: query
schema:
type: string
format: uuid
description: Filter by domain ID.
- name: reply_to_sent_email_id
in: query
schema:
type: string
format: uuid
description: |
Filter to inbound emails that are replies to a specific
outbound send. The value is a `sent_emails.id` (UUID). At
inbound ingest, Primitive matches the parsed In-Reply-To
header (or References as a fallback) against
`sent_emails.message_id` in the same org and records the
resolved id on `emails.reply_to_sent_email_id`. This filter
is the strict-threading lookup behind `primitive chat` and
any UI that wants to show the inbound reply to a given
send. NULL on inbound that isn't a threaded reply to one
of your sends, so existing emails received before this
ingestion landed will not match.
- name: status
in: query
schema:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
description: Filter by inbound email lifecycle status.
- name: date_from
in: query
schema:
type: string
format: date-time
description: Filter emails received on or after this timestamp.
- name: date_to
in: query
schema:
type: string
format: date-time
description: Filter emails received on or before this timestamp.
- name: has_attachment
in: query
schema:
type: string
enum:
- "true"
- "false"
description: Filter by whether the email has one or more attachments.
- name: spam_score_lt
in: query
schema:
type: number
description: Filter to emails with spam score below this value.
- name: spam_score_gte
in: query
schema:
type: number
description: Filter to emails with spam score greater than or equal to this value.
- name: sort
in: query
schema:
type: string
enum:
- relevance
- received_at_desc
- received_at_asc
description: |
Sort mode. Defaults to relevance when a text query is present,
otherwise `received_at_desc`.
- name: cursor
in: query
schema:
type: string
maxLength: 200
description: Opaque pagination cursor from a previous search response.
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
description: Number of results per page
- name: snippet
in: query
schema:
type: string
enum:
- "true"
- "false"
default: "true"
description: Include subject/body highlight snippets when text search is active.
- name: include_facets
in: query
schema:
type: string
enum:
- "true"
- "false"
default: "true"
description: Include facet counts for sender, domain, status, and attachment presence.
responses:
"200":
description: Search results
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: array
items:
allOf:
- type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
status:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. For most legitimate mail this equals the bare
address in the From header; for mailing lists, bounce
handlers, and forwarders it is typically the bounce address
rather than the human-visible sender.
For the parsed From-header value (with display name handling
and a sender-fallback when the header is unparseable), GET
the email by id and use `from_email`.
recipient:
type: string
subject:
type:
- string
- "null"
domain:
type: string
spam_score:
type:
- number
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
raw_size_bytes:
type:
- integer
- "null"
webhook_status:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
webhook_attempt_count:
type: integer
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Fetch
`/threads/{thread_id}` for the full ordered thread. NULL on
messages received before threading was enabled.
required:
- id
- status
- sender
- recipient
- domain
- created_at
- received_at
- webhook_attempt_count
- type: object
properties:
attachment_count:
type: integer
description: Number of parsed attachments on the email.
from_known_address:
type: boolean
description: Whether the parsed From address is known to this org from prior authenticated inbound mail.
score:
type: number
description: Relevance score. Present only when sorting by relevance.
highlights:
type: object
properties:
subject:
type: array
items:
type: string
description: Subject snippets with matching terms highlighted.
body:
type: array
items:
type: string
description: Body snippets with matching terms highlighted.
required:
- subject
- body
required:
- attachment_count
- from_known_address
meta:
type: object
properties:
total:
type: integer
description: Total number of matching records, capped when `total_capped` is true.
total_capped:
type: boolean
description: Whether `total` was capped instead of counted exactly.
limit:
type: integer
description: Page size used for this request.
cursor:
type:
- string
- "null"
description: Cursor for the next search page, or null if no more results.
sort:
type: string
enum:
- relevance
- received_at_desc
- received_at_asc
description: Sort mode used for the result page.
required:
- total
- total_capped
- limit
- cursor
- sort
facets:
type: object
properties:
by_sender:
type: array
items:
type: object
properties:
value:
type:
- string
- "null"
count:
type: integer
required:
- value
- count
by_domain:
type: array
items:
type: object
properties:
value:
type:
- string
- "null"
count:
type: integer
required:
- value
- count
by_status:
type: array
items:
type: object
properties:
value:
type:
- string
- "null"
count:
type: integer
required:
- value
- count
has_attachment:
type: object
properties:
"true":
type: integer
"false":
type: integer
required:
- "true"
- "false"
required:
- by_sender
- by_domain
- by_status
- has_attachment
required:
- data
- meta
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"504":
$ref: "#/components/responses/GatewayTimeout"
description: Search query timed out
security:
- BearerAuth: []
/emails/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: getEmail
summary: Get inbound email by id
description: |
Returns the full record for an inbound email received at one
of your verified domains, including the parsed text and HTML
bodies, threading metadata, SMTP envelope detail, webhook
delivery state, and a `replies` array for any outbound sends
recorded as replies to this inbound.
For listing inbound emails (with cursor pagination, status
and date filters, and free-text search), use
`/emails`. Outbound (sent) email records are NOT returned
here; use `/sent-emails/{id}` for those.
The response carries four sender-shaped fields whose
meanings overlap. `from_email` is the canonical "who sent
this" field for most use cases (parsed bare address from
the `From:` header, with a `sender` fallback). `from_header`
is the raw header including any display name. `sender` and
`smtp_mail_from` both carry the SMTP envelope MAIL FROM
(return-path) and are equal by construction; `sender` is
the older field name retained for compatibility. See
`primitive describe emails:get-email | jq '.responseSchema.properties'`
for per-field detail.
tags:
- Emails
responses:
"200":
description: Email details
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. Same value as `smtp_mail_from`; both fields exist
so protocol-aware tooling can use whichever name it expects.
For most legitimate mail this equals `from_email`; for
mailing lists, bounce handlers, and forwarders it is
typically the bounce-handling address rather than the
human-visible sender.
**For the canonical "who sent this email" value, use
`from_email`.**
recipient:
type: string
subject:
type:
- string
- "null"
body_text:
type:
- string
- "null"
description: Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed.
body_html:
type:
- string
- "null"
description: HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed.
status:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
domain:
type: string
spam_score:
type:
- number
- "null"
raw_size_bytes:
type:
- integer
- "null"
raw_sha256:
type:
- string
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
rejection_reason:
type:
- string
- "null"
webhook_status:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
webhook_attempt_count:
type: integer
webhook_last_attempt_at:
type:
- string
- "null"
format: date-time
webhook_last_status_code:
type:
- integer
- "null"
webhook_last_error:
type:
- string
- "null"
webhook_fired_at:
type:
- string
- "null"
format: date-time
smtp_helo:
type:
- string
- "null"
smtp_mail_from:
type:
- string
- "null"
description: |
SMTP envelope MAIL FROM (return-path), as accepted by the
inbound mail server. Same value as `sender`; both fields
exist so protocol-aware tooling can use whichever name it
expects.
For the canonical "who sent this email" value (display name
stripped, From-header preferred), use `from_email`.
smtp_rcpt_to:
type:
- array
- "null"
items:
type: string
from_header:
type:
- string
- "null"
description: |
Raw `From:` header from the message body, including any
display name (e.g. `"Alice Example" `).
Use this when you need the display name for rendering.
For the bare email address (display name stripped), use
`from_email`.
content_discarded_at:
type:
- string
- "null"
format: date-time
content_discarded_by_delivery_id:
type:
- string
- "null"
from_email:
type: string
description: |
Bare email address parsed from the `From:` header, with
display name stripped (e.g. `[email protected]`). Falls
back to `sender` (the SMTP envelope MAIL FROM) when the
`From:` header cannot be parsed.
**This is the canonical "who sent this email" field for
most use cases**, including comparing against allowlists,
routing replies, or displaying the sender to a user. Use
`from_header` when you specifically need the display name,
or `sender`/`smtp_mail_from` when you need the SMTP
envelope value (e.g. to follow a bounce).
to_email:
type: string
description: Parsed to address (same as recipient)
from_known_address:
type: boolean
description: |
True when the inbound's sender address has a matching grant
in the org's known-send-addresses list. Advisory: a true
value does not by itself guarantee that a reply will be
accepted by send-mail's gates; the per-send check at send
time remains authoritative.
replies:
type: array
description: |
Sent emails recorded as replies to this inbound, in send
order (ascending). Populated when a customer's send-mail
request carries an `in_reply_to` Message-ID that matches
this inbound's `message_id` in the same org. Includes
attempts that were gate-denied, so the array reflects every
recorded reply attempt regardless of outcome.
items:
type: object
properties:
id:
type: string
format: uuid
description: Sent-email row id.
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
to_address:
type: string
description: Recipient address as recorded on the sent_emails row.
subject:
type:
- string
- "null"
created_at:
type: string
format: date-time
queue_id:
type:
- string
- "null"
description: Outbound relay queue identifier when available.
required:
- id
- status
- to_address
- created_at
reply_to_sent_email_id:
type:
- string
- "null"
format: uuid
description: |
The `sent_emails.id` of the outbound this inbound was a
reply to, when resolvable. Set at inbound ingest by
matching the parsed In-Reply-To (or References, as a
fallback) against `sent_emails.message_id` in the same
org. The mirror of `sent_emails.in_reply_to_email_id` for
the inbound side of a thread. NULL when the inbound is
not a threaded reply to one of your sends, when neither
header survived the path through intermediate MTAs, or on
inbound received before this auto-link landed.
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Inbound and
outbound messages in the same conversation share a
`thread_id`; fetch `/threads/{thread_id}` for the full
ordered thread. Assigned at ingest. NULL on messages
received before threading was enabled (until backfilled).
parsed:
allOf:
- type: object
description: |
Parsed MIME content for an inbound email. Mirrors the
`email.parsed` object on the webhook payload so a single parser
handles both surfaces. `status` is `complete` when parsing
succeeded; on `failed` the body/address/attachment fields are
absent and `error` describes why.
properties:
status:
type: string
enum:
- complete
- failed
body_text:
type:
- string
- "null"
description: Plain-text body. Present when `status` is `complete`.
body_html:
type:
- string
- "null"
description: HTML body. Present when `status` is `complete`.
reply_to:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `Reply-To` header addresses.
cc:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `Cc` header addresses.
bcc:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `Bcc` header addresses (rarely present on inbound).
to_addresses:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `To` header addresses.
in_reply_to:
type:
- array
- "null"
items:
type: string
description: Message-IDs from the `In-Reply-To` header.
references:
type:
- array
- "null"
items:
type: string
description: Message-IDs from the `References` header.
attachments:
type: array
items:
type: object
description: |
Metadata for one attachment. The bytes are not inline; download
all attachments for a message as a gzipped tarball via
`/emails/{id}/attachments.tar.gz`. `sha256` lets you verify a
specific part after extraction.
properties:
filename:
type:
- string
- "null"
content_type:
type:
- string
- "null"
size_bytes:
type: integer
sha256:
type:
- string
- "null"
part_index:
type: integer
description: Zero-based index of this part within the message.
required:
- size_bytes
description: Attachment metadata. Empty array when none.
error:
type:
- object
- "null"
description: |
Present (non-null) only when `status` is `failed`. When
present, all three fields are populated, so a consumer can
branch on `code` without defensive null checks.
properties:
code:
type: string
description: Stable failure code (e.g. `PARSE_FAILED`).
message:
type: string
retryable:
type: boolean
required:
- code
- message
- retryable
required:
- status
description: |
Parsed MIME content (addresses, threading headers,
attachment metadata), matching the `email.parsed` object
on the webhook payload so one parser handles both the
webhook and this endpoint. The top-level `body_text` /
`body_html` fields above are the same values as
`parsed.body_text` / `parsed.body_html`, retained for
backward compatibility.
auth:
allOf:
- type: object
description: |
SPF / DKIM / DMARC verdicts computed at ingest. Mirrors the
`email.auth` object on the webhook payload. Field names are
camelCase to match that payload exactly. For messages received
before auth was recorded, the verdicts default to `none`.
properties:
spf:
type: string
description: SPF result (e.g. `pass`, `fail`, `softfail`, `none`).
dmarc:
type: string
description: DMARC result (e.g. `pass`, `fail`, `none`).
dmarcPolicy:
type:
- string
- "null"
description: Published DMARC policy (`none`, `quarantine`, `reject`).
dmarcFromDomain:
type:
- string
- "null"
description: The From-header domain DMARC was evaluated against.
dmarcSpfAligned:
type: boolean
dmarcDkimAligned:
type: boolean
dmarcSpfStrict:
type:
- boolean
- "null"
dmarcDkimStrict:
type:
- boolean
- "null"
dkimSignatures:
type: array
items:
type: object
description: One DKIM signature found on the message, with its verdict.
properties:
domain:
type: string
selector:
type: string
result:
type: string
description: Verification result (e.g. `pass`, `fail`, `none`).
aligned:
type: boolean
description: Whether the signing domain aligns with the From domain (for DMARC).
keyBits:
type:
- integer
- "null"
algo:
type:
- string
- "null"
required:
- domain
- selector
- result
- aligned
required:
- spf
- dmarc
- dmarcSpfAligned
- dmarcDkimAligned
- dkimSignatures
description: |
SPF / DKIM / DMARC verdicts computed at ingest, matching
the `email.auth` object on the webhook payload. Use these
to decide how much to trust a message before acting on
instructions it contains.
required:
- id
- sender
- recipient
- status
- domain
- created_at
- received_at
- webhook_attempt_count
- from_email
- to_email
- replies
- parsed
- auth
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteEmail
summary: Delete an email
tags:
- Emails
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
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
description: Permanently delete a received email and its stored content. This cannot be undone.
/emails/{id}/raw:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: downloadRawEmail
summary: Download raw email
description: |
Downloads the raw RFC 822 email file (.eml). Authenticates via
a signed download token (provided in webhook payloads) or a
valid session.
tags:
- Emails
security:
- BearerAuth: []
- DownloadToken: []
parameters:
- name: token
in: query
schema:
type: string
description: Signed download token from webhook payload
responses:
"200":
description: Raw email file
content:
message/rfc822:
schema:
type: string
format: binary
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
Content-Disposition:
schema:
type: string
example: attachment; filename="email-id.eml"
X-Content-SHA256:
schema:
type: string
description: SHA-256 hex digest of the file
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
/emails/{id}/attachments.tar.gz:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: downloadAttachments
summary: Download email attachments
description: |
Downloads all attachments as a gzip-compressed tar archive.
Authenticates via a signed download token (provided in webhook
payloads) or a valid session.
tags:
- Emails
security:
- BearerAuth: []
- DownloadToken: []
parameters:
- name: token
in: query
schema:
type: string
description: Signed download token from webhook payload
responses:
"200":
description: Attachments archive
content:
application/gzip:
schema:
type: string
format: binary
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
Content-Disposition:
schema:
type: string
example: attachment; filename="email-id_attachments.tar.gz"
X-Content-SHA256:
schema:
type: string
description: SHA-256 hex digest of the archive
X-Attachment-Count:
schema:
type: string
description: Number of attachments in the archive
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
/emails/{id}/reply:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: replyToEmail
summary: Reply to an inbound email
description: |
Sends an outbound reply to the inbound email identified by `id`.
Threading headers (`In-Reply-To`, `References`), recipient
derivation (Reply-To, then From, then bare sender), and the
`Re:` subject prefix are all derived server-side from the
stored inbound row. The request body carries only the message
body, optional From override, optional attachments, and optional
`wait` flag; passing any header or recipient override is
rejected by the schema (`additionalProperties: false`).
Forwards through the same gates as `/send-mail`: the response
status, error envelope, and `idempotent_replay` flag mirror
the send-mail contract verbatim.
servers:
- url: https://api.primitive.dev/v1
description: Canonical API host (recommended)
- url: https://www.primitive.dev/api/v1
description: Legacy compatibility host (Vercel body limit applies)
tags:
- Sending
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: |
Body shape for `/emails/{id}/reply`. Intentionally narrow:
recipients (`to`), subject, and threading headers
(`in_reply_to`, `references`) are derived server-side from
the inbound row referenced by the path id and are rejected by
`additionalProperties` if passed (returns 400).
`from` IS allowed because of legitimate use cases (display-name
addition, replying from a different verified outbound address,
multi-team triage). Send-mail's per-send `canSendFrom` gate
validates the from-domain regardless, so the override carries
no extra privilege.
properties:
body_text:
type: string
description: Plain-text reply body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes (same cap as send-mail).
body_html:
type: string
description: HTML reply body. At least one of body_text or body_html is required.
from:
type: string
minLength: 3
maxLength: 998
description: |
Optional override for the reply's From header. Defaults to
the inbound's recipient. Use to add a display name (`"Acme
Support" `) or to reply from a different
verified outbound address (e.g. multi-team routing where
support@ triages to billing@). The from-domain must be a
verified outbound domain for your org, same as send-mail.
wait:
type: boolean
description: When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics.
attachments:
type: array
maxItems: 100
description: Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.
items:
type: object
additionalProperties: false
properties:
filename:
type: string
minLength: 1
maxLength: 255
description: Attachment filename. Control characters are rejected.
content_type:
type: string
minLength: 1
maxLength: 255
description: Optional MIME content type. Control characters are rejected.
content_base64:
type: string
minLength: 1
maxLength: 44040192
description: Base64-encoded attachment bytes.
required:
- filename
- content_base64
responses:
"200":
description: Outbound relay result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
description: Persisted sent-email attempt ID.
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
from:
type: string
description: |
Bare from-address actually written on the wire. Echoed
on every success branch so callers can confirm what
went out, particularly useful for the /emails/{id}/reply
path where `from` is server-derived from the inbound's
recipient when the caller doesn't override.
For sends where the caller passed a from-header that
included a display name (e.g. `"Acme Support" `),
this field is the parsed bare address (`[email protected]`).
The display name was sent on the wire intact; this field
just makes the address easy to compare against allowlists.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's OUTBOUND relay
(the box that signs your mail and submits it to the
receiving MTA). NOT the receiver's queue id.
The receiver may also report its own queue id in
`smtp_response_text` (e.g. `"250 2.0.0 Ok: queued as
99D111927CDA"` from a Postfix receiver). Those two ids
refer to different mail systems and are NOT comparable.
Treat `queue_id` as Primitive-internal and the
receiver's id as remote-system-internal.
Null on rows that never reached the relay (queued,
gate_denied, agent_failed before signing).
accepted:
type: array
items:
type: string
description: Recipient addresses accepted by the relay.
rejected:
type: array
items:
type: string
description: Recipient addresses rejected by the relay.
client_idempotency_key:
type: string
description: Effective idempotency key used for this send.
request_id:
type: string
description: Server-issued request identifier for support and tracing.
content_hash:
type: string
description: Stable hash of the canonical send payload.
delivery_status:
type: string
description: |
Narrower enum covering only the four terminal delivery
outcomes returned to a synchronous `wait: true` send.
On the SendMailResult shape, `delivery_status` is always
equal to `status` whenever both are present (i.e. on
terminal-state replays and live wait=true responses).
The two fields exist so callers that want to type-narrow
on "this is a delivery outcome" can pattern-match against
the four-value enum without handling the broader
SentEmailStatus value set (which also covers `queued`,
`submitted_to_agent`, `agent_failed`, `gate_denied`,
`unknown`).
On async-mode and pre-terminal responses, `delivery_status`
is absent and only `status` is populated. Use `status` if
you want a single field that's always present.
enum:
- delivered
- bounced
- deferred
- wait_timeout
smtp_response_code:
type:
- integer
- "null"
description: SMTP response code from the first downstream delivery outcome when wait is true.
smtp_response_text:
type: string
description: SMTP response text from the first downstream delivery outcome when wait is true.
idempotent_replay:
type: boolean
description: |
True when the response replays a previously-recorded send
keyed by `client_idempotency_key` (same key, same canonical
payload). False on a fresh send and on gate-denied
responses. Lets callers branch on cache state without
diffing fields.
required:
- id
- status
- from
- queue_id
- accepted
- rejected
- client_idempotency_key
- request_id
- content_hash
- idempotent_replay
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: |
Inbound is not repliable: the row exists but lacks a
`message_id` (no thread anchor) or a `recipient` (cannot
derive the From address).
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
"500":
$ref: "#/components/responses/InternalError"
description: Primitive encountered an internal error
"502":
$ref: "#/components/responses/BadGateway"
description: Primitive could not complete the downstream SMTP request
"503":
$ref: "#/components/responses/ServiceUnavailable"
description: Primitive is temporarily unable to process the request
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
/emails/{id}/replay:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: replayEmailWebhooks
summary: Replay email webhooks
description: |
Re-delivers the webhook payload for this email to all active
endpoints matching the email's domain. Rate limited per-email
(short cooldown between successive replays of the same email)
and per-org (burst + sustained windows), sharing an org-wide
budget with delivery replays.
tags:
- Emails
responses:
"200":
description: Replay result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
delivered:
type: integer
description: Number of successful deliveries
failed:
type: integer
description: Number of failed deliveries
required:
- delivered
- failed
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/emails/{id}/discard-content:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: discardEmailContent
summary: Discard email content
description: |
Permanently deletes the email's raw bytes, parsed body (text + HTML),
and attachments while preserving metadata (sender, recipient,
subject, timestamps, hashes, attachment manifest) for audit logs.
Idempotent: a second call returns success with
`already_discarded: true` and does no work.
**Gated** on the customer's discard-content opt-in (managed in the
dashboard at Settings > Webhooks). When the toggle is off, this
endpoint returns `403` with code `discard_not_enabled` and a
message pointing the human at the dashboard. There is intentionally
no API to flip this toggle. Opting in to a destructive,
non-reversible operation must be a deliberate human click in the
UI.
tags:
- Emails
responses:
"200":
description: Discard result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
discarded:
type: boolean
description: |
Always `true` on a 2xx response. The content is either now
discarded as a result of this call, or was already discarded
before this call ran.
already_discarded:
type: boolean
description: |
`true` if the email's content was already discarded before
this call ran (no work was done). `false` if this call was
the one that performed the discard.
required:
- discarded
- already_discarded
required:
- data
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
"500":
$ref: "#/components/responses/InternalError"
description: Primitive encountered an internal error
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
/emails/{id}/conversation:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: getConversation
summary: Get the conversation an email belongs to
description: |
Returns the full conversation the given inbound email belongs
to, as ordered, ready-to-prompt turns WITH bodies. It resolves
the thread from the email and returns every message oldest-first,
so an agent that received an email can pass `messages` straight
to a chat model in one call instead of walking `/threads/{id}`
plus `/emails/{id}` and `/sent-emails/{id}` per message.
Each message carries a `direction` (`inbound` | `outbound`) and a
derived `role`: `inbound` -> `user`, `outbound` -> `assistant`
(your own prior replies). The role mapping assumes the caller
owns the outbound side, which is the agent-reply case this exists
for. If the email has no thread yet (a brand-new message), the
conversation is just that one message as a single user turn.
The message list is capped; check `truncated` to detect when
older messages were omitted. Consecutive same-role turns are not
merged here; that normalization is model-specific and left to the
caller.
tags:
- Emails
responses:
"200":
description: Conversation
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
The full conversation an inbound email belongs to, as ordered,
ready-to-prompt turns with bodies. Resolves the thread from the
email and returns every message oldest-first, so an agent that
received an email can pass `messages` straight to a chat model in
one call.
properties:
thread_id:
type:
- string
- "null"
format: uuid
description: |
The thread this email belongs to, or null when the email
isn't threaded yet (the conversation is then just this one
message).
subject:
type:
- string
- "null"
description: |
Normalized thread subject (Re/Fwd prefixes stripped), or the
email's own subject when it isn't threaded.
message_count:
type: integer
description: |
Total messages in the thread. `messages` is capped, so
`truncated` is true (and this can exceed `messages.length`)
when older messages were omitted.
truncated:
type: boolean
description: |
True when `messages` omits part of the conversation because
the thread exceeds the per-call cap.
messages:
type: array
items:
type: object
description: One message in the conversation, with its body and a chat role.
properties:
role:
type: string
enum:
- user
- assistant
description: |
Chat role derived from `direction`: `user` for inbound
(received) messages, `assistant` for outbound (your own prior
replies). Lets `messages` be passed directly to a chat model.
direction:
type: string
enum:
- inbound
- outbound
description: |
`inbound` for a received email (`/emails/{id}`), `outbound`
for a send (`/sent-emails/{id}`).
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
from:
type:
- string
- "null"
to:
type:
- string
- "null"
subject:
type:
- string
- "null"
text:
type: string
description: |
Plain-text body. Empty string when the message has no text
part or its content was discarded by retention.
timestamp:
type:
- string
- "null"
format: date-time
description: received_at for inbound, created_at for outbound.
required:
- role
- direction
- id
- text
required:
- thread_id
- message_count
- truncated
- messages
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/endpoints:
get:
operationId: listEndpoints
summary: List webhook endpoints
description: Returns all active (non-deleted) webhook endpoints.
tags:
- Endpoints
responses:
"200":
description: List of endpoints
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
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
url:
type:
- string
- "null"
enabled:
type: boolean
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict this endpoint to emails from a specific domain
rules:
type: object
description: Endpoint-specific filtering rules
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
delivery_count:
type: integer
description: Total webhook deliveries attempted
success_count:
type: integer
description: Successful deliveries
failure_count:
type: integer
description: Failed deliveries
consecutive_fails:
type: integer
description: Current streak of consecutive failures
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
deactivated_at:
type:
- string
- "null"
format: date-time
kind:
type: string
enum:
- http
- function
description: "http: deliver to the webhook URL. function: invoke a Primitive Function."
function_id:
type:
- string
- "null"
format: uuid
description: The Function this endpoint invokes, when kind is function.
is_route_target:
type: boolean
description: |
When true, this endpoint is reachable only via an explicit recipient
route, never as a domain's default destination, and is exempt from
the one-endpoint-per-domain rule (so many can share a domain).
required:
- id
- org_id
- enabled
- rules
- created_at
- updated_at
- delivery_count
- success_count
- failure_count
- consecutive_fails
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: createEndpoint
summary: Create a webhook endpoint
description: |
Creates a new webhook endpoint. If a deactivated endpoint
with the same URL and domain exists, it is reactivated
instead. Subject to plan limits on the number of active
endpoints.
**Signing is account-scoped, not per-endpoint.** This call
does not return any signing material; every endpoint on the
account uses the same webhook secret, fetched via
`GET /account/webhook-secret`. See the API-level "Webhook
signing" section for the full wire format (header name,
signed string, hash algo, secret format, tolerance) and a
language-agnostic verification recipe.
After creating the endpoint, fire a test delivery against
it via `POST /endpoints/{id}/test` to confirm your verifier
accepts the signature.
tags:
- Endpoints
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
kind:
type: string
enum:
- http
- function
default: http
description: "http: deliver to a webhook URL (provide url). function: invoke a Primitive Function (provide function_id, omit url)."
url:
type: string
minLength: 1
description: The webhook URL to deliver events to. Required when kind is http; omit for function endpoints.
function_id:
type: string
format: uuid
description: The Function to invoke. Required when kind is function.
enabled:
type: boolean
default: true
description: Whether the endpoint is active
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict to emails from a specific domain
rules:
type: object
description: Endpoint-specific filtering rules
is_route_target:
type: boolean
default: false
description: |
Create this endpoint as a route-target: reachable only via an
explicit recipient route, never a domain's default destination, and
exempt from the one-endpoint-per-domain rule.
responses:
"201":
description: Endpoint created (or reactivated)
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
url:
type:
- string
- "null"
enabled:
type: boolean
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict this endpoint to emails from a specific domain
rules:
type: object
description: Endpoint-specific filtering rules
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
delivery_count:
type: integer
description: Total webhook deliveries attempted
success_count:
type: integer
description: Successful deliveries
failure_count:
type: integer
description: Failed deliveries
consecutive_fails:
type: integer
description: Current streak of consecutive failures
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
deactivated_at:
type:
- string
- "null"
format: date-time
kind:
type: string
enum:
- http
- function
description: "http: deliver to the webhook URL. function: invoke a Primitive Function."
function_id:
type:
- string
- "null"
format: uuid
description: The Function this endpoint invokes, when kind is function.
is_route_target:
type: boolean
description: |
When true, this endpoint is reachable only via an explicit recipient
route, never as a domain's default destination, and is exempt from
the one-endpoint-per-domain rule (so many can share a domain).
required:
- id
- org_id
- enabled
- rules
- created_at
- updated_at
- delivery_count
- success_count
- failure_count
- consecutive_fails
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
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
/endpoints/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
patch:
operationId: updateEndpoint
summary: Update a webhook endpoint
description: |
Updates an active webhook endpoint. If the URL is changed, the old
endpoint is deactivated and a new one is created (or an existing
deactivated endpoint with the new URL is reactivated).
tags:
- Endpoints
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
url:
type: string
minLength: 1
description: New webhook URL (triggers endpoint rotation)
enabled:
type: boolean
domain_id:
type:
- string
- "null"
format: uuid
rules:
type: object
minProperties: 1
responses:
"200":
description: Updated endpoint
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
url:
type:
- string
- "null"
enabled:
type: boolean
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict this endpoint to emails from a specific domain
rules:
type: object
description: Endpoint-specific filtering rules
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
delivery_count:
type: integer
description: Total webhook deliveries attempted
success_count:
type: integer
description: Successful deliveries
failure_count:
type: integer
description: Failed deliveries
consecutive_fails:
type: integer
description: Current streak of consecutive failures
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
deactivated_at:
type:
- string
- "null"
format: date-time
kind:
type: string
enum:
- http
- function
description: "http: deliver to the webhook URL. function: invoke a Primitive Function."
function_id:
type:
- string
- "null"
format: uuid
description: The Function this endpoint invokes, when kind is function.
is_route_target:
type: boolean
description: |
When true, this endpoint is reachable only via an explicit recipient
route, never as a domain's default destination, and is exempt from
the one-endpoint-per-domain rule (so many can share a domain).
required:
- id
- org_id
- enabled
- rules
- created_at
- updated_at
- delivery_count
- success_count
- failure_count
- consecutive_fails
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteEndpoint
summary: Delete a webhook endpoint
description: Delete a webhook endpoint. Inbound messages are no longer delivered to it.
tags:
- Endpoints
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
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/endpoints/{id}/test:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: testEndpoint
summary: Send a test webhook
description: |
Sends a sample `email.received` event to the endpoint. The request
includes SSRF protection (private IP rejection and DNS pinning).
Rate limited to 4 per minute and 30 per hour (non-exempt).
Successful deliveries and verified-domain endpoints are exempt
from the rate limit.
tags:
- Endpoints
responses:
"200":
description: Test result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
status:
type: integer
description: HTTP status code returned by the endpoint
body:
type: string
description: Response body (truncated to 1000 characters)
signature:
type: string
description: The signature header value sent (if webhook secret is configured)
required:
- status
- body
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/filters:
get:
operationId: listFilters
summary: List filter rules
description: Returns all whitelist and blocklist filter rules.
tags:
- Filters
responses:
"200":
description: List of filters
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
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: If set, filter applies only to this domain
type:
type: string
enum:
- whitelist
- blocklist
pattern:
type: string
description: Email address or pattern to match (stored lowercase)
enabled:
type: boolean
created_at:
type: string
format: date-time
required:
- id
- org_id
- type
- pattern
- enabled
- created_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: createFilter
summary: Create a filter rule
description: |
Creates a new whitelist or blocklist filter. Per-domain filters
require a Pro plan. Patterns are stored as lowercase.
tags:
- Filters
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- whitelist
- blocklist
pattern:
type: string
minLength: 1
maxLength: 500
description: Email address or pattern to filter
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict filter to a specific domain (Pro plan required)
required:
- type
- pattern
responses:
"201":
description: Filter created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: If set, filter applies only to this domain
type:
type: string
enum:
- whitelist
- blocklist
pattern:
type: string
description: Email address or pattern to match (stored lowercase)
enabled:
type: boolean
created_at:
type: string
format: date-time
required:
- id
- org_id
- type
- pattern
- enabled
- created_at
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
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
/filters/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
patch:
operationId: updateFilter
summary: Update a filter rule
description: Toggle a filter's enabled state.
tags:
- Filters
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: Updated filter
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: If set, filter applies only to this domain
type:
type: string
enum:
- whitelist
- blocklist
pattern:
type: string
description: Email address or pattern to match (stored lowercase)
enabled:
type: boolean
created_at:
type: string
format: date-time
required:
- id
- org_id
- type
- pattern
- enabled
- created_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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteFilter
summary: Delete a filter rule
tags:
- Filters
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
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
description: Delete an inbound routing filter rule. Messages already processed by the rule are unaffected.
/wake/schedules:
get:
operationId: listWakeSchedules
summary: List wake schedules
description: Returns the org's wake.dispatch schedules.
tags:
- Wake
responses:
"200":
description: List of wake schedules
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: A cron schedule that sends a wake.dispatch command to a function.
properties:
id:
type: string
format: uuid
from_address:
type:
- string
- "null"
description: The sending identity the wake is signed as.
target_address:
type: string
description: The function address the wake is delivered to.
command:
type: string
args:
type: object
additionalProperties: true
cron_expr:
type: string
description: 5-field cron expression.
timezone:
type: string
description: IANA timezone the cron is evaluated in.
next_run_at:
type: string
format: date-time
last_run_at:
type:
- string
- "null"
format: date-time
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- target_address
- command
- cron_expr
- timezone
- next_run_at
- enabled
- 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: createWakeSchedule
summary: Create a wake schedule
description: |
Create a cron schedule that sends a wake.dispatch command to one of your
own function addresses. `from` and `to` must differ (no self-dispatch);
the cron expression and IANA timezone are validated and the first fire
time is computed without firing immediately.
tags:
- Wake
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
from_address:
type: string
description: Sending identity (must be a domain the org can sign).
target_address:
type: string
description: Your function address (must differ from from_address).
command:
type: string
minLength: 1
maxLength: 200
args:
type: object
additionalProperties: true
description: Optional JSON object passed through to the woken function.
cron_expr:
type: string
minLength: 1
maxLength: 120
timezone:
type: string
minLength: 1
maxLength: 64
default: UTC
note:
type: string
maxLength: 2000
required:
- from_address
- target_address
- command
- cron_expr
responses:
"201":
description: Wake schedule created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A cron schedule that sends a wake.dispatch command to a function.
properties:
id:
type: string
format: uuid
from_address:
type:
- string
- "null"
description: The sending identity the wake is signed as.
target_address:
type: string
description: The function address the wake is delivered to.
command:
type: string
args:
type: object
additionalProperties: true
cron_expr:
type: string
description: 5-field cron expression.
timezone:
type: string
description: IANA timezone the cron is evaluated in.
next_run_at:
type: string
format: date-time
last_run_at:
type:
- string
- "null"
format: date-time
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- target_address
- command
- cron_expr
- timezone
- next_run_at
- enabled
- created_at
- updated_at
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
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
/wake/schedules/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: getWakeSchedule
summary: Get a wake schedule
tags:
- Wake
responses:
"200":
description: The wake schedule
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A cron schedule that sends a wake.dispatch command to a function.
properties:
id:
type: string
format: uuid
from_address:
type:
- string
- "null"
description: The sending identity the wake is signed as.
target_address:
type: string
description: The function address the wake is delivered to.
command:
type: string
args:
type: object
additionalProperties: true
cron_expr:
type: string
description: 5-field cron expression.
timezone:
type: string
description: IANA timezone the cron is evaluated in.
next_run_at:
type: string
format: date-time
last_run_at:
type:
- string
- "null"
format: date-time
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- target_address
- command
- cron_expr
- timezone
- next_run_at
- enabled
- 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: []
description: Get a wake schedule
patch:
operationId: updateWakeSchedule
summary: Update a wake schedule
description: |
Update a schedule's command, args, cadence, addresses, note, or enabled
state. Changing the cadence (or re-enabling) recomputes the next fire time.
tags:
- Wake
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
enabled:
type: boolean
command:
type: string
minLength: 1
maxLength: 200
args:
type: object
additionalProperties: true
cron_expr:
type: string
minLength: 1
maxLength: 120
timezone:
type: string
minLength: 1
maxLength: 64
from_address:
type: string
target_address:
type: string
note:
type:
- string
- "null"
maxLength: 2000
responses:
"200":
description: Updated wake schedule
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A cron schedule that sends a wake.dispatch command to a function.
properties:
id:
type: string
format: uuid
from_address:
type:
- string
- "null"
description: The sending identity the wake is signed as.
target_address:
type: string
description: The function address the wake is delivered to.
command:
type: string
args:
type: object
additionalProperties: true
cron_expr:
type: string
description: 5-field cron expression.
timezone:
type: string
description: IANA timezone the cron is evaluated in.
next_run_at:
type: string
format: date-time
last_run_at:
type:
- string
- "null"
format: date-time
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- target_address
- command
- cron_expr
- timezone
- next_run_at
- enabled
- 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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteWakeSchedule
summary: Delete a wake schedule
tags:
- Wake
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
security:
- BearerAuth: []
description: Delete a wake schedule
/wake/schedules/{id}/run:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: runWakeSchedule
summary: Run a wake schedule now
description: |
Fire the schedule immediately, sending one wake.dispatch via the same
signed-send path as a scheduled fire. Does not change the schedule's next
fire time.
tags:
- Wake
responses:
"200":
description: The wake interaction that was emitted
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
additionalProperties: true
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
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
/wake/authorizations:
get:
operationId: listWakeAuthorizations
summary: List wake authorizations
description: |
Returns the per-target allowlist grants that authorize which senders may
wake a function. Optionally filter by the target endpoint.
tags:
- Wake
parameters:
- name: recipient_endpoint_id
in: query
required: false
schema:
type: string
format: uuid
description: Only return grants for this target endpoint
responses:
"200":
description: List of wake authorizations
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: A per-target allowlist grant authorizing a sender to wake a function.
properties:
id:
type: string
format: uuid
recipient_endpoint_id:
type: string
format: uuid
allowed_sender_domain:
type: string
allowed_sender_address:
type:
- string
- "null"
allowed_commands:
type:
- array
- "null"
items:
type: string
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- recipient_endpoint_id
- allowed_sender_domain
- enabled
- created_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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
security:
- BearerAuth: []
post:
operationId: createWakeAuthorization
summary: Create a wake authorization
description: |
Grant a sender domain (and optionally a specific address and command set)
permission to wake a target function. The domain must be fully-qualified.
tags:
- Wake
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
recipient_endpoint_id:
type: string
format: uuid
allowed_sender_domain:
type: string
minLength: 1
maxLength: 253
description: Fully-qualified sender domain (at least two labels).
allowed_sender_address:
type:
- string
- "null"
description: Optional specific sender address to pin the grant to.
allowed_commands:
type:
- array
- "null"
maxItems: 64
items:
type: string
minLength: 1
maxLength: 200
description: Optional command allowlist; null = any command.
note:
type: string
maxLength: 2000
required:
- recipient_endpoint_id
- allowed_sender_domain
responses:
"201":
description: Wake authorization created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A per-target allowlist grant authorizing a sender to wake a function.
properties:
id:
type: string
format: uuid
recipient_endpoint_id:
type: string
format: uuid
allowed_sender_domain:
type: string
allowed_sender_address:
type:
- string
- "null"
allowed_commands:
type:
- array
- "null"
items:
type: string
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- recipient_endpoint_id
- allowed_sender_domain
- enabled
- created_at
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
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
/wake/authorizations/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
patch:
operationId: updateWakeAuthorization
summary: Update a wake authorization
description: Toggle a wake authorization's enabled state.
tags:
- Wake
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: Updated wake authorization
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A per-target allowlist grant authorizing a sender to wake a function.
properties:
id:
type: string
format: uuid
recipient_endpoint_id:
type: string
format: uuid
allowed_sender_domain:
type: string
allowed_sender_address:
type:
- string
- "null"
allowed_commands:
type:
- array
- "null"
items:
type: string
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- recipient_endpoint_id
- allowed_sender_domain
- enabled
- created_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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteWakeAuthorization
summary: Delete a wake authorization
tags:
- Wake
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
security:
- BearerAuth: []
description: Delete a wake authorization
/wake/dispatches:
get:
operationId: listWakeDispatches
summary: List recent wake dispatches
description: Read-only audit of recent wake.dispatch interactions for the org.
tags:
- Wake
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 50
description: Maximum number of rows to return (1-200, default 50)
responses:
"200":
description: List of recent wake dispatches
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: A recorded wake.dispatch interaction (audit row).
properties:
id:
type: string
format: uuid
wire_id:
type: string
role:
type: string
state:
type: string
outcome:
type:
- string
- "null"
awaiting:
type:
- string
- "null"
counterparty_address:
type: string
our_address:
type: string
step_count:
type: integer
created_at:
type: string
format: date-time
completed_at:
type:
- string
- "null"
format: date-time
required:
- id
- wire_id
- role
- state
- counterparty_address
- our_address
- created_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: []
/routes:
get:
operationId: listRoutes
summary: List recipient routes
description: |
Returns the org's recipient routing rules in evaluation order. Each rule
binds a recipient address pattern to one endpoint; inbound mail resolves
to a single destination at delivery time.
tags:
- Routes
responses:
"200":
description: List of routes
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: A recipient routing rule binding an address pattern to one endpoint.
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: Domain the route is scoped to; null = org-wide.
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
description: The recipient address pattern (an exact address or a wildcard).
pattern_norm:
type:
- string
- "null"
description: Normalized pattern used for matching.
endpoint_id:
type: string
format: uuid
description: The endpoint inbound mail matching this rule is delivered to.
priority:
type: integer
description: Evaluation order within a scope; lower is checked first.
enabled:
type: boolean
match_count:
type: string
description: How many emails have matched this rule (a bigint, returned as a string).
last_matched_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
required:
- id
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: createRoute
summary: Create a recipient route
description: |
Binds a recipient pattern to a destination. Provide exactly one of
`endpoint_id` (an existing endpoint) or `function_id`. With `function_id`,
a dedicated route-target endpoint is minted for that function in the same
transaction, enabling per-address function routing (e.g.
`[email protected] -> functionA`).
tags:
- Routes
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: |
Provide exactly one of `endpoint_id` or `function_id`. With `function_id`,
a route-target endpoint is minted for that function and the route is bound
to it in one transaction.
properties:
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
minLength: 1
maxLength: 512
endpoint_id:
type: string
format: uuid
description: An existing endpoint to route to. Mutually exclusive with function_id.
function_id:
type: string
format: uuid
description: Route to this function, minting its route-target endpoint if needed. Mutually exclusive with endpoint_id.
domain_id:
type:
- string
- "null"
format: uuid
description: Scope the route to a domain; defaults to the pattern's domain.
priority:
type: integer
minimum: 0
maximum: 1000000
enabled:
type: boolean
required:
- match_type
- pattern
responses:
"201":
description: Route created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A recipient routing rule binding an address pattern to one endpoint.
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: Domain the route is scoped to; null = org-wide.
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
description: The recipient address pattern (an exact address or a wildcard).
pattern_norm:
type:
- string
- "null"
description: Normalized pattern used for matching.
endpoint_id:
type: string
format: uuid
description: The endpoint inbound mail matching this rule is delivered to.
priority:
type: integer
description: Evaluation order within a scope; lower is checked first.
enabled:
type: boolean
match_count:
type: string
description: How many emails have matched this rule (a bigint, returned as a string).
last_matched_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
required:
- id
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
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
/routes/reorder:
post:
operationId: reorderRoutes
summary: Reorder recipient routes
description: Update the priority of one or more routes in a single call.
tags:
- Routes
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
updates:
type: array
minItems: 1
maxItems: 1000
items:
type: object
additionalProperties: false
properties:
id:
type: string
format: uuid
priority:
type: integer
minimum: 0
maximum: 1000000
required:
- id
- priority
required:
- updates
responses:
"200":
description: Updated route list
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: A recipient routing rule binding an address pattern to one endpoint.
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: Domain the route is scoped to; null = org-wide.
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
description: The recipient address pattern (an exact address or a wildcard).
pattern_norm:
type:
- string
- "null"
description: Normalized pattern used for matching.
endpoint_id:
type: string
format: uuid
description: The endpoint inbound mail matching this rule is delivered to.
priority:
type: integer
description: Evaluation order within a scope; lower is checked first.
enabled:
type: boolean
match_count:
type: string
description: How many emails have matched this rule (a bigint, returned as a string).
last_matched_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
required:
- id
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
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
/routes/simulate:
post:
operationId: simulateRoute
summary: Simulate routing for a recipient
description: |
Resolves where an inbound email to `recipient` would be delivered, with a
trace of every rule evaluated and why. Read-only; creates nothing.
tags:
- Routes
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
recipient:
type: string
minLength: 1
maxLength: 320
event_type:
type: string
minLength: 1
maxLength: 100
description: Event type to model; defaults to email.received.
required:
- recipient
responses:
"200":
description: Routing decision
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: Where an inbound email to the recipient would be delivered, and why.
properties:
outcome:
type: string
enum:
- matched
- defaulted
- none
recipient:
type: string
endpoint_id:
type:
- string
- "null"
matched_route_id:
type:
- string
- "null"
matched_tier:
type:
- string
- "null"
enum:
- exact
- wildcard
- regex
- null
matched_pattern:
type:
- string
- "null"
default_scope:
type:
- string
- "null"
enum:
- domain
- org
- null
evaluated:
type: array
items:
type: object
properties:
route_id:
type: string
tier:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
result:
type: string
enum:
- hit
- miss
- skipped
- error
reason:
type: string
required:
- route_id
- tier
- pattern
- result
truncated:
type: boolean
required:
- outcome
- recipient
- endpoint_id
- matched_route_id
- matched_tier
- matched_pattern
- default_scope
- evaluated
- truncated
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
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
/routes/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
patch:
operationId: updateRoute
summary: Update a recipient route
tags:
- Routes
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
minLength: 1
maxLength: 512
endpoint_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
priority:
type: integer
minimum: 0
maximum: 1000000
enabled:
type: boolean
responses:
"200":
description: Updated route
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: A recipient routing rule binding an address pattern to one endpoint.
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: Domain the route is scoped to; null = org-wide.
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
description: The recipient address pattern (an exact address or a wildcard).
pattern_norm:
type:
- string
- "null"
description: Normalized pattern used for matching.
endpoint_id:
type: string
format: uuid
description: The endpoint inbound mail matching this rule is delivered to.
priority:
type: integer
description: Evaluation order within a scope; lower is checked first.
enabled:
type: boolean
match_count:
type: string
description: How many emails have matched this rule (a bigint, returned as a string).
last_matched_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
required:
- id
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
security:
- BearerAuth: []
description: Update a recipient route
delete:
operationId: deleteRoute
summary: Delete a recipient route
tags:
- Routes
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
security:
- BearerAuth: []
description: Delete a recipient route
/webhooks/deliveries:
get:
operationId: listDeliveries
summary: List webhook deliveries
description: |
Returns a paginated list of webhook delivery attempts. Each delivery
includes a nested `email` object with sender, recipient, and subject.
tags:
- Webhook Deliveries
parameters:
- name: cursor
in: query
schema:
type: string
description: |
Pagination cursor from a previous response's `meta.cursor` field.
Format: `{ISO-datetime}|{id}`
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
description: Number of results per page
- name: email_id
in: query
schema:
type: string
format: uuid
description: Filter by email ID
- name: status
in: query
schema:
type: string
enum:
- pending
- delivered
- header_confirmed
- failed
description: Filter by delivery status
- name: date_from
in: query
schema:
type: string
format: date-time
description: Filter deliveries created on or after this timestamp
- name: date_to
in: query
schema:
type: string
format: date-time
description: Filter deliveries created on or before this timestamp
responses:
"200":
description: Paginated list of deliveries
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
meta:
type: object
properties:
total:
type: integer
description: Total number of matching records
limit:
type: integer
description: Page size used for this request
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if no more results
required:
- total
- limit
- cursor
required:
- success
- data
- meta
- type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
description: Delivery ID (numeric string)
email_id:
type: string
format: uuid
org_id:
type: string
format: uuid
endpoint_id:
type: string
format: uuid
endpoint_url:
type: string
status:
type: string
enum:
- pending
- delivered
- header_confirmed
- failed
attempt_count:
type: integer
duration_ms:
type:
- integer
- "null"
last_error:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
email:
type:
- object
- "null"
properties:
sender:
type: string
recipient:
type: string
subject:
type:
- string
- "null"
required:
- sender
- recipient
required:
- id
- email_id
- org_id
- endpoint_id
- endpoint_url
- status
- attempt_count
- 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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
security:
- BearerAuth: []
/webhooks/deliveries/{id}/replay:
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: ^\d+$
description: Delivery ID (numeric)
post:
operationId: replayDelivery
summary: Replay a webhook delivery
description: |
Re-sends the stored webhook payload from a previous delivery attempt.
If the original endpoint is still active, it is targeted. If the
original endpoint was deleted, the oldest active endpoint is used.
Deactivated endpoints cannot be replayed to. Rate limited per-org,
sharing an org-wide budget with email replays.
tags:
- Webhook Deliveries
responses:
"200":
description: Replay result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
delivered:
type: integer
description: Number of successful deliveries
failed:
type: integer
description: Number of failed deliveries
required:
- delivered
- failed
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/send-permissions:
get:
operationId: getSendPermissions
summary: List send-permission rules
description: |
Returns a flat list of rules describing every recipient the
caller may send to. Each rule has a `type`, a kind-specific
payload, and a human-readable `description`. If any rule
matches the recipient, /send-mail will accept the send under
the recipient-scope check.
The endpoint is the answer to "where can I send" without
exposing internal entitlement names. Agents that don't
recognize a `type` can still read the `description` prose
and act on it.
Rule kinds, ordered broadest-first so an agent can stop
scanning at the first match:
1. `any_recipient` (one entry, only when the org can send
anywhere): every other rule below it is redundant.
2. `managed_zone` (always emitted, one per Primitive-managed
zone): sends to any address at *.primitive.email or
*.email.works always succeed; no entitlement required.
3. `your_domain` (one per active verified outbound domain
owned by the org): sends to that domain are approved.
4. `address` (one per address that has authenticated
inbound mail to the org, capped at `meta.address_cap`):
sends to that exact address are approved.
The list is informational, not an authorization check.
/send-mail remains the source of truth on whether an
individual send will succeed (it also enforces the
from-address and the `send_mail` entitlement, which are
not recipient-scope concerns and are not represented here).
tags:
- Sending
responses:
"200":
description: Send-permission rules for the caller's org
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: array
items:
description: |
One recipient-scope rule describing a destination the caller
may send to. Discriminated on `type`. Each rule carries a
human-prose `description` field intended for display.
Rule kinds are stable within an SDK release. A response
containing a `type` value not enumerated in this schema
means the server is running a newer version than the SDK;
upgrade the SDK to the release that matches the server's
schema. Strict-parsing SDKs (Go, Python) will raise a
decode error in that case rather than silently dropping
the unknown rule, since silent drops would let an outbound
agent reason from an incomplete view of its own permissions.
discriminator:
propertyName: type
mapping:
any_recipient: "#/components/schemas/SendPermissionAnyRecipient"
managed_zone: "#/components/schemas/SendPermissionManagedZone"
your_domain: "#/components/schemas/SendPermissionYourDomain"
address: "#/components/schemas/SendPermissionAddress"
oneOf:
- type: object
description: |
The caller can send to any recipient. When this rule is
present, every other rule in the response is redundant.
properties:
type:
type: string
enum:
- any_recipient
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- description
- type: object
description: |
The caller can send to any address at the named
Primitive-managed zone. Always emitted (no entitlement
required) because Primitive owns the zone and every mailbox
belongs to a Primitive customer by construction.
properties:
type:
type: string
enum:
- managed_zone
zone:
type: string
description: |
The managed apex domain. Sends are accepted to any
address at the apex itself or any subdomain (e.g.
`[email protected]` and `[email protected]`
both match the `primitive.email` zone rule).
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- zone
- description
- type: object
description: |
The caller can send to any address at one of their own
verified outbound domains. Emitted once per active row in
the org's `domains` table.
properties:
type:
type: string
enum:
- your_domain
domain:
type: string
description: A verified outbound domain owned by the caller's org.
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- domain
- description
- type: object
description: |
The caller can send to a specific address that has
authenticated inbound mail to the org. Emitted once per row
in the org's `known_send_addresses` table, capped at
`meta.address_cap`.
properties:
type:
type: string
enum:
- address
address:
type: string
description: The bare email address this rule grants sends to.
last_received_at:
type: string
format: date-time
description: |
Most recent inbound email from this address that
authenticated successfully (DMARC pass + DKIM/SPF
alignment). Updated on each new authenticated receipt.
received_count:
type: integer
description: |
Total number of authenticated inbound emails from this
address. Increments only when `last_received_at` advances.
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- address
- last_received_at
- received_count
- description
meta:
type: object
description: |
Response metadata for /send-permissions. The `address_cap`
bounds the size of the `address` rule subset; orgs with more
than `address_cap` known addresses almost always also hold a
broader rule type (`any_recipient` or `your_domain`), so the
cap is a response-size bound rather than a meaningful
product limit.
properties:
address_cap:
type: integer
description: Maximum number of `address` rules included in `data`.
truncated:
type: boolean
description: |
True when the org has more than `address_cap` known
addresses and the list was truncated. False when every
known address is represented or when the org holds no
address rules at all.
required:
- address_cap
- truncated
required:
- data
- meta
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: []
/send-mail:
post:
operationId: sendEmail
summary: Send outbound email
description: |
Sends an outbound email through Primitive's outbound relay. By default
the request returns once the relay accepts the message for delivery.
Set `wait: true` to wait for the first downstream SMTP delivery outcome.
**Host routing.** /send-mail is served by the canonical API host
(`https://api.primitive.dev/v1`) so the request body can carry
inline attachments up to ~30 MiB raw. The legacy dashboard
compatibility host (`https://www.primitive.dev/api/v1`) also accepts
/send-mail, but Vercel request body limits apply before proxying.
The typed SDKs route /send-mail to the canonical API host
automatically.
servers:
- url: https://api.primitive.dev/v1
description: Canonical API host (recommended)
- url: https://www.primitive.dev/api/v1
description: Legacy compatibility host (Vercel body limit applies)
tags:
- Sending
parameters:
- name: Idempotency-Key
in: header
required: false
schema:
type: string
minLength: 1
maxLength: 255
pattern: ^[\x21-\x7E]+$
description: |
Optional customer-supplied idempotency key. If omitted, Primitive
derives one from the canonical request payload and echoes the
effective value in the `Idempotency-Key` response header.
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
from:
type: string
minLength: 3
maxLength: 998
description: RFC 5322 From header. The sender domain must be a verified outbound domain for your organization.
to:
type: string
minLength: 3
maxLength: 320
description: Recipient address. Recipient eligibility depends on your account's outbound entitlements.
subject:
type: string
minLength: 1
maxLength: 998
description: Subject line for the outbound message
body_text:
type: string
description: Plain-text message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
body_html:
type: string
description: HTML message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
in_reply_to:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
description: Message-ID of the direct parent email when sending a threaded reply.
references:
type: array
maxItems: 100
description: Full ordered message-id chain for the thread.
items:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
attachments:
type: array
maxItems: 100
description: Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.
items:
type: object
additionalProperties: false
properties:
filename:
type: string
minLength: 1
maxLength: 255
description: Attachment filename. Control characters are rejected.
content_type:
type: string
minLength: 1
maxLength: 255
description: Optional MIME content type. Control characters are rejected.
content_base64:
type: string
minLength: 1
maxLength: 44040192
description: Base64-encoded attachment bytes.
required:
- filename
- content_base64
wait:
type: boolean
description: When true, wait for the first downstream SMTP delivery outcome before returning.
wait_timeout_ms:
type: integer
minimum: 1000
maximum: 30000
description: Maximum time to wait for a delivery outcome when wait is true. Defaults to 30000.
required:
- from
- to
- subject
responses:
"200":
description: Outbound relay result
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
description: Persisted sent-email attempt ID.
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
from:
type: string
description: |
Bare from-address actually written on the wire. Echoed
on every success branch so callers can confirm what
went out, particularly useful for the /emails/{id}/reply
path where `from` is server-derived from the inbound's
recipient when the caller doesn't override.
For sends where the caller passed a from-header that
included a display name (e.g. `"Acme Support" `),
this field is the parsed bare address (`[email protected]`).
The display name was sent on the wire intact; this field
just makes the address easy to compare against allowlists.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's OUTBOUND relay
(the box that signs your mail and submits it to the
receiving MTA). NOT the receiver's queue id.
The receiver may also report its own queue id in
`smtp_response_text` (e.g. `"250 2.0.0 Ok: queued as
99D111927CDA"` from a Postfix receiver). Those two ids
refer to different mail systems and are NOT comparable.
Treat `queue_id` as Primitive-internal and the
receiver's id as remote-system-internal.
Null on rows that never reached the relay (queued,
gate_denied, agent_failed before signing).
accepted:
type: array
items:
type: string
description: Recipient addresses accepted by the relay.
rejected:
type: array
items:
type: string
description: Recipient addresses rejected by the relay.
client_idempotency_key:
type: string
description: Effective idempotency key used for this send.
request_id:
type: string
description: Server-issued request identifier for support and tracing.
content_hash:
type: string
description: Stable hash of the canonical send payload.
delivery_status:
type: string
description: |
Narrower enum covering only the four terminal delivery
outcomes returned to a synchronous `wait: true` send.
On the SendMailResult shape, `delivery_status` is always
equal to `status` whenever both are present (i.e. on
terminal-state replays and live wait=true responses).
The two fields exist so callers that want to type-narrow
on "this is a delivery outcome" can pattern-match against
the four-value enum without handling the broader
SentEmailStatus value set (which also covers `queued`,
`submitted_to_agent`, `agent_failed`, `gate_denied`,
`unknown`).
On async-mode and pre-terminal responses, `delivery_status`
is absent and only `status` is populated. Use `status` if
you want a single field that's always present.
enum:
- delivered
- bounced
- deferred
- wait_timeout
smtp_response_code:
type:
- integer
- "null"
description: SMTP response code from the first downstream delivery outcome when wait is true.
smtp_response_text:
type: string
description: SMTP response text from the first downstream delivery outcome when wait is true.
idempotent_replay:
type: boolean
description: |
True when the response replays a previously-recorded send
keyed by `client_idempotency_key` (same key, same canonical
payload). False on a fresh send and on gate-denied
responses. Lets callers branch on cache state without
diffing fields.
required:
- id
- status
- from
- queue_id
- accepted
- rejected
- client_idempotency_key
- request_id
- content_hash
- idempotent_replay
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
Idempotency-Key:
description: Effective idempotency key for this request. When the caller supplies an `Idempotency-Key` request header the same value is echoed; otherwise the server derives one from the canonical payload hash. Use this to correlate a request to its stored sent-email record via the `idempotency_key` filter on `GET /sent-emails`.
schema:
type: string
minLength: 1
maxLength: 255
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
"500":
$ref: "#/components/responses/InternalError"
description: Primitive encountered an internal error
"502":
$ref: "#/components/responses/BadGateway"
description: Primitive could not complete the downstream SMTP request
"503":
$ref: "#/components/responses/ServiceUnavailable"
description: Primitive is temporarily unable to process the request
security:
- BearerAuth: []
/semantic-search:
post:
operationId: semanticSearch
summary: Semantic search across received and sent mail
description: |
Ranked search across both received and sent mail. The `mode`
field selects the ranking strategy:
- `keyword`: lexical full-text matching only (no embeddings).
- `semantic`: meaning-based matching using vector embeddings.
- `hybrid` (default): blends the semantic and keyword signals.
Results are ordered by a relevance `score`. Every row reports the
fields it matched (`matched_fields`), a match-centered excerpt per
field (`snippets`), and a `score_breakdown` whose components account
for the `score`. Page through results by passing the prior
response's `meta.cursor` back as `cursor`.
Requires the Pro plan and the `semantic_search_enabled`
entitlement; callers without them receive `403`.
Host routing: this operation is served only by the search host
(`https://api.primitive.dev/v1`). The typed SDKs route it there
automatically.
servers:
- url: https://api.primitive.dev/v1
description: Search host
tags:
- Search
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
query:
type: string
minLength: 1
maxLength: 2048
description: |
Free-text query. Required for `semantic` and `hybrid` modes;
optional for `keyword` mode.
mode:
type: string
enum:
- hybrid
- semantic
- keyword
default: hybrid
description: |
Ranking strategy. `keyword` is lexical only, `semantic` is
embedding-based, `hybrid` blends both.
corpus:
type: array
items:
type: string
enum:
- inbound
- outbound
minItems: 1
maxItems: 2
description: |
Which mail to search. Defaults to both received (`inbound`)
and sent (`outbound`).
search_in:
type: array
items:
type: string
enum:
- subject
- headers
- addresses
- body
description: A searchable email field.
description: Restrict matching to these fields. Defaults to all.
exclude:
type: array
items:
type: string
enum:
- subject
- headers
- addresses
- body
description: A searchable email field.
description: Exclude these fields from matching.
date_from:
type: string
format: date-time
description: Only include mail at or after this timestamp.
date_to:
type: string
format: date-time
description: Only include mail at or before this timestamp.
include:
type: array
items:
type: string
enum:
- coverage
description: |
Opt-in extras. `coverage` adds an index-coverage snapshot to
`meta`. Matched fields, snippets, and the score breakdown are
always returned regardless of this field.
limit:
type: integer
minimum: 1
maximum: 100
default: 10
description: Maximum number of results to return.
cursor:
type: string
description: Opaque pagination cursor from a prior response's `meta.cursor`.
responses:
"200":
description: Ranked search results
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
properties:
source_type:
type: string
enum:
- inbound_email
- sent_email
description: Whether this row is a received or sent message.
id:
type: string
description: Message id. Combine with `api_url` to fetch the full record.
subject:
type:
- string
- "null"
from:
type:
- string
- "null"
to:
type:
- string
- "null"
timestamp:
type: string
description: Message timestamp (received_at for inbound, created_at for sent).
status:
type: string
description: Lifecycle status of the message.
score:
type: number
description: Overall relevance score; the `score_breakdown` components account for it.
semantic_score:
type:
- number
- "null"
description: Raw semantic similarity signal, or null when not applicable.
keyword_score:
type:
- number
- "null"
description: Raw keyword (lexical) signal, or null when not applicable.
matched_fields:
type: array
items:
type: string
enum:
- subject
- headers
- addresses
- body
description: A searchable email field.
description: Fields where the query matched.
snippets:
type: array
items:
type: object
properties:
field:
type: string
description: The field this excerpt came from.
text:
type: string
description: Plain-text excerpt centered on the match (no markup).
required:
- field
- text
description: Match-centered excerpts, one per matched field.
score_breakdown:
type: object
description: |
Additive contributions to `score`. `semantic` and `keyword` are the
raw signals times the mode's weight (null when not applicable);
these plus `field_boost` and `recency` sum to `score` before each
value is independently rounded to 5 decimal places.
properties:
semantic:
type:
- number
- "null"
keyword:
type:
- number
- "null"
field_boost:
type: number
recency:
type: number
required:
- semantic
- keyword
- field_boost
- recency
api_url:
type:
- string
- "null"
description: Relative API path to fetch the full message.
required:
- source_type
- id
- subject
- from
- to
- timestamp
- status
- score
- semantic_score
- keyword_score
- matched_fields
- snippets
- score_breakdown
- api_url
meta:
type: object
properties:
limit:
type: integer
description: Page size used for this request.
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if there are no more results.
mode:
type: string
enum:
- hybrid
- semantic
- keyword
description: Ranking mode used for this response.
coverage:
oneOf:
- type: object
description: Index-coverage snapshot for the org, returned only when the `coverage` include option is requested.
properties:
embedded_chunks:
type: integer
pending_chunks:
type: integer
skipped_plan_chunks:
type: integer
skipped_quota_chunks:
type: integer
unsupported_attachment_chunks:
type: integer
failed_chunks:
type: integer
required:
- embedded_chunks
- pending_chunks
- skipped_plan_chunks
- skipped_quota_chunks
- unsupported_attachment_chunks
- failed_chunks
- type: "null"
description: |
Index-coverage snapshot, present only when requested via
`include: [coverage]`; otherwise null.
required:
- limit
- cursor
- mode
- coverage
required:
- data
- meta
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
"500":
$ref: "#/components/responses/InternalError"
description: Primitive encountered an internal error
"503":
$ref: "#/components/responses/ServiceUnavailable"
description: Primitive is temporarily unable to process the request
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
/sent-emails:
get:
operationId: listSentEmails
summary: List outbound sent emails
description: |
Returns a paginated list of OUTBOUND emails the caller's
org has sent via /send-mail (and /emails/{id}/reply, which
forwards through /send-mail). Includes every recorded
attempt, including gate-denied attempts that the agent
never called and rows still in `queued` state.
For inbound mail received at your verified domains, see
/emails. There is no unified send/receive history endpoint;
the two surfaces are intentionally separate because the
underlying tables, statuses, and lifecycle differ.
Email bodies (`body_text`, `body_html`) are NOT included on
list rows so a 50-row page can't balloon into a multi-MB
response when sends are near the 5MB body cap. Use
/sent-emails/{id} to fetch a single row with bodies, or
cross-reference by `client_idempotency_key` if the caller
already has the body locally.
tags:
- Sending
parameters:
- name: cursor
in: query
schema:
type: string
description: |
Pagination cursor from a previous response's `meta.cursor` field.
Format: `{ISO-datetime}|{id}`
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
description: Number of results per page
- name: status
in: query
schema:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
description: |
Filter to rows in this status. Useful for polling
queued rows that haven't transitioned, auditing
gate-denied attempts, or listing only successful
deliveries.
- name: request_id
in: query
schema:
type: string
format: uuid
description: |
Filter to the row matching a specific server-issued
`request_id`. The /send-mail response surfaces
`request_id` on every send; this lookup lets the
caller find the historical row for a given live call
without remembering its `id`.
- name: idempotency_key
in: query
schema:
type: string
minLength: 1
maxLength: 255
description: |
Filter to rows with the given `client_idempotency_key`.
Multiple rows can share a key (a retry that hit the
idempotent-replay path returns the same row, but a
retry with a DIFFERENT canonical payload under the
same key is rejected by /send-mail before the row is
written, so duplicates are bounded).
- name: date_from
in: query
schema:
type: string
format: date-time
description: Inclusive lower bound on `created_at`.
- name: date_to
in: query
schema:
type: string
format: date-time
description: Inclusive upper bound on `created_at`.
responses:
"200":
description: Page of sent-email summaries
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
meta:
type: object
properties:
total:
type: integer
description: Total number of matching records
limit:
type: integer
description: Page size used for this request
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if no more results
required:
- total
- limit
- cursor
required:
- success
- data
- meta
- type: object
properties:
data:
type: array
items:
type: object
description: |
List-row projection of a sent-email record. Drops
`body_text` and `body_html` to keep paginated responses
small; fetch /sent-emails/{id} for the full record with
bodies.
properties:
id:
type: string
format: uuid
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
status_changed_at:
type: string
format: date-time
description: |
Timestamp of the most recent status transition.
Polling clients should treat `status='queued'` AND
`status_changed_at` older than 5 minutes as
"stuck-queued" (the post-tx UPDATE failed and the
actual delivery state is recoverable from on-box logs
via `queue_id` when populated, or `request_id`).
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
client_idempotency_key:
type:
- string
- "null"
description: |
Effective idempotency key used for this send. If the
caller passed the `Idempotency-Key` header, this is
that value; otherwise it's a server-derived hash of
the canonical request payload.
content_hash:
type: string
description: Stable hash of the canonical send payload.
from_header:
type: string
description: |
Raw `From:` header as sent on the wire, including any
display name (e.g. `"Acme Support" `).
from_address:
type: string
description: Bare email address parsed from `from_header`.
to_header:
type: string
description: |
Raw `To:` header as sent on the wire, including any
display name.
to_address:
type: string
description: Bare email address parsed from `to_header`.
subject:
type: string
body_size_bytes:
type: integer
description: |
Total UTF-8 byte length of `body_text` + `body_html`.
Surfaced on the list endpoint so callers can see "this
row has a 4MB body" without fetching it.
content_discarded_at:
type:
- string
- "null"
format: date-time
description: |
Timestamp at which the bodies were discarded by an
entitlement-driven retention policy. Null when bodies
are still present. The detail endpoint returns
null-valued `body_text`/`body_html` for discarded rows.
message_id:
type:
- string
- "null"
description: |
Wire-level Message-ID assigned to the outbound message
(RFC 5322). Null on rows that never reached signing
(queued, gate_denied, agent_failed before signing).
in_reply_to:
type:
- string
- "null"
description: |
Wire-level In-Reply-To header value, when this send
was a reply.
email_references:
type:
- string
- "null"
description: |
Wire-level References header value, when this send
was a reply.
in_reply_to_email_id:
type:
- string
- "null"
format: uuid
description: |
Reference to the inbound `emails.id` that this send
replied to, when known. Populated when the caller used
/emails/{id}/reply or when /send-mail's `in_reply_to`
matched a stored inbound message_id in the same org.
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this send belongs to. A reply inherits
the thread of the inbound it answers; a fresh send starts a
new thread. Fetch `/threads/{thread_id}` for the full
ordered thread (inbound + outbound interleaved). NULL on
gate-denied sends and on sends created before threading was
enabled.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's outbound
relay once the agent accepts the message. Null on
queued, gate_denied, and agent_failed rows.
smtp_response_code:
type:
- integer
- "null"
description: |
Receiver's 3-digit SMTP code (e.g. 250, 550, 451).
Populated on terminal delivery statuses; may be null
on a deferred where the agent never got an SMTP-level
response (TCP refused, DNS failed, TLS handshake
failed). `smtp_response_text` still carries Postfix's
descriptive text in those cases.
smtp_response_text:
type:
- string
- "null"
description: |
Free-form text portion of the receiver's SMTP
response. The most useful debugging signal on a
`bounced` or `deferred` row.
smtp_enhanced_status_code:
type:
- string
- "null"
description: |
RFC 3463 enhanced status code (e.g. `5.1.1` for "Bad
destination mailbox address"). Distinct from
`smtp_response_code`: the basic 3-digit code is coarse
(550 = "permanent failure"), the enhanced code is
finer-grained.
dkim_selector:
type:
- string
- "null"
description: |
DKIM selector used to sign the outbound message.
Public DNS data; useful for diagnosing why a downstream
verifier rejected the signature.
dkim_domain:
type:
- string
- "null"
description: DKIM signing domain.
error_code:
type:
- string
- "null"
description: |
Stable public error code on `agent_failed` rows. The
agent's internal codes are remapped to a stable public
taxonomy (see `publicAgentError` in the server) so this
field is safe to branch on across agent versions.
error_message:
type:
- string
- "null"
description: Free-form error message accompanying `error_code`.
gates:
type:
- array
- "null"
items:
type: object
properties:
name:
type: string
enum:
- send_to_confirmed_domains
- send_to_known_addresses
description: Public recipient-scope gate name that denied the send.
reason:
type: string
enum:
- domain_not_confirmed
- recipient_unauthenticated
- recipient_not_known
description: Stable machine-readable denial reason.
message:
type: string
description: Human-readable explanation of the gate denial.
subject:
type: string
description: Domain or address the gate evaluated.
fix:
type: object
properties:
action:
type: string
enum:
- confirm_domain
- sender_must_fix_authentication
- wait_for_inbound
description: Suggested next action for the caller.
subject:
type: string
description: Entity the action applies to.
required:
- action
- subject
docs_url:
type: string
description: Public docs URL with more context.
required:
- name
- reason
- message
- subject
description: |
Gate-denial detail on `gate_denied` rows. Mirrors the
synchronous /send-mail 403 contract so a caller's
GateDenial handler is the same across live denies and
historical lookups. Null on every other status.
request_id:
type:
- string
- "null"
description: |
Server-issued request identifier from the original
/send-mail call. Surfaced as the `X-Request-Id`
response header on the live send and recorded here
for support escalation.
required:
- id
- status
- status_changed_at
- created_at
- updated_at
- content_hash
- from_header
- from_address
- to_header
- to_address
- subject
- body_size_bytes
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
security:
- BearerAuth: []
/sent-emails/{id}:
get:
operationId: getSentEmail
summary: Get a sent email by id
description: |
Returns the full sent-email record by id, including
`body_text` and `body_html` (omitted from the listing
endpoint to keep paginated responses small). Use this when
diagnosing a specific send, e.g. inspecting the receiver's
SMTP response on a `bounced` row or pulling the gate
denial detail on a `gate_denied` row.
tags:
- Sending
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
responses:
"200":
description: Sent-email detail
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
description: |
Full sent-email record, including `body_text` and
`body_html`. Returned by /sent-emails/{id}.
allOf:
- type: object
description: |
List-row projection of a sent-email record. Drops
`body_text` and `body_html` to keep paginated responses
small; fetch /sent-emails/{id} for the full record with
bodies.
properties:
id:
type: string
format: uuid
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
status_changed_at:
type: string
format: date-time
description: |
Timestamp of the most recent status transition.
Polling clients should treat `status='queued'` AND
`status_changed_at` older than 5 minutes as
"stuck-queued" (the post-tx UPDATE failed and the
actual delivery state is recoverable from on-box logs
via `queue_id` when populated, or `request_id`).
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
client_idempotency_key:
type:
- string
- "null"
description: |
Effective idempotency key used for this send. If the
caller passed the `Idempotency-Key` header, this is
that value; otherwise it's a server-derived hash of
the canonical request payload.
content_hash:
type: string
description: Stable hash of the canonical send payload.
from_header:
type: string
description: |
Raw `From:` header as sent on the wire, including any
display name (e.g. `"Acme Support" `).
from_address:
type: string
description: Bare email address parsed from `from_header`.
to_header:
type: string
description: |
Raw `To:` header as sent on the wire, including any
display name.
to_address:
type: string
description: Bare email address parsed from `to_header`.
subject:
type: string
body_size_bytes:
type: integer
description: |
Total UTF-8 byte length of `body_text` + `body_html`.
Surfaced on the list endpoint so callers can see "this
row has a 4MB body" without fetching it.
content_discarded_at:
type:
- string
- "null"
format: date-time
description: |
Timestamp at which the bodies were discarded by an
entitlement-driven retention policy. Null when bodies
are still present. The detail endpoint returns
null-valued `body_text`/`body_html` for discarded rows.
message_id:
type:
- string
- "null"
description: |
Wire-level Message-ID assigned to the outbound message
(RFC 5322). Null on rows that never reached signing
(queued, gate_denied, agent_failed before signing).
in_reply_to:
type:
- string
- "null"
description: |
Wire-level In-Reply-To header value, when this send
was a reply.
email_references:
type:
- string
- "null"
description: |
Wire-level References header value, when this send
was a reply.
in_reply_to_email_id:
type:
- string
- "null"
format: uuid
description: |
Reference to the inbound `emails.id` that this send
replied to, when known. Populated when the caller used
/emails/{id}/reply or when /send-mail's `in_reply_to`
matched a stored inbound message_id in the same org.
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this send belongs to. A reply inherits
the thread of the inbound it answers; a fresh send starts a
new thread. Fetch `/threads/{thread_id}` for the full
ordered thread (inbound + outbound interleaved). NULL on
gate-denied sends and on sends created before threading was
enabled.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's outbound
relay once the agent accepts the message. Null on
queued, gate_denied, and agent_failed rows.
smtp_response_code:
type:
- integer
- "null"
description: |
Receiver's 3-digit SMTP code (e.g. 250, 550, 451).
Populated on terminal delivery statuses; may be null
on a deferred where the agent never got an SMTP-level
response (TCP refused, DNS failed, TLS handshake
failed). `smtp_response_text` still carries Postfix's
descriptive text in those cases.
smtp_response_text:
type:
- string
- "null"
description: |
Free-form text portion of the receiver's SMTP
response. The most useful debugging signal on a
`bounced` or `deferred` row.
smtp_enhanced_status_code:
type:
- string
- "null"
description: |
RFC 3463 enhanced status code (e.g. `5.1.1` for "Bad
destination mailbox address"). Distinct from
`smtp_response_code`: the basic 3-digit code is coarse
(550 = "permanent failure"), the enhanced code is
finer-grained.
dkim_selector:
type:
- string
- "null"
description: |
DKIM selector used to sign the outbound message.
Public DNS data; useful for diagnosing why a downstream
verifier rejected the signature.
dkim_domain:
type:
- string
- "null"
description: DKIM signing domain.
error_code:
type:
- string
- "null"
description: |
Stable public error code on `agent_failed` rows. The
agent's internal codes are remapped to a stable public
taxonomy (see `publicAgentError` in the server) so this
field is safe to branch on across agent versions.
error_message:
type:
- string
- "null"
description: Free-form error message accompanying `error_code`.
gates:
type:
- array
- "null"
items:
type: object
properties:
name:
type: string
enum:
- send_to_confirmed_domains
- send_to_known_addresses
description: Public recipient-scope gate name that denied the send.
reason:
type: string
enum:
- domain_not_confirmed
- recipient_unauthenticated
- recipient_not_known
description: Stable machine-readable denial reason.
message:
type: string
description: Human-readable explanation of the gate denial.
subject:
type: string
description: Domain or address the gate evaluated.
fix:
type: object
properties:
action:
type: string
enum:
- confirm_domain
- sender_must_fix_authentication
- wait_for_inbound
description: Suggested next action for the caller.
subject:
type: string
description: Entity the action applies to.
required:
- action
- subject
docs_url:
type: string
description: Public docs URL with more context.
required:
- name
- reason
- message
- subject
description: |
Gate-denial detail on `gate_denied` rows. Mirrors the
synchronous /send-mail 403 contract so a caller's
GateDenial handler is the same across live denies and
historical lookups. Null on every other status.
request_id:
type:
- string
- "null"
description: |
Server-issued request identifier from the original
/send-mail call. Surfaced as the `X-Request-Id`
response header on the live send and recorded here
for support escalation.
required:
- id
- status
- status_changed_at
- created_at
- updated_at
- content_hash
- from_header
- from_address
- to_header
- to_address
- subject
- body_size_bytes
- type: object
properties:
body_text:
type:
- string
- "null"
description: |
Plain-text body sent on the wire. Null when the
send carried only an HTML body, or when bodies have
been discarded post-send (`content_discarded_at`
set).
body_html:
type:
- string
- "null"
description: |
HTML body sent on the wire. Null when the send
carried only a plain-text body, or when bodies
have been discarded post-send.
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/threads/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: getThread
summary: Get a conversation thread by id
description: |
Returns a conversation thread: its metadata plus the inbound
and outbound messages that belong to it, interleaved in time
order (oldest first). A thread spans both received emails and
your sends, so an agent can reconstruct an entire back-and-forth
from one call instead of walking reply headers.
Each message carries a `direction` (`inbound` | `outbound`) and
an `id`; fetch the full message via `/emails/{id}` or
`/sent-emails/{id}` accordingly. Bodies are omitted here to keep
the thread view lightweight.
Discover a thread id from the `thread_id` field on any email or
sent-email (list or detail). The message list is capped; compare
`message_count` against `messages.length` to detect truncation.
tags:
- Threads
responses:
"200":
description: Thread detail
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
A conversation thread: its metadata plus the inbound and
outbound messages that belong to it, interleaved oldest-first.
Membership is the stored `thread_id` on each message. Bodies are
omitted here to keep the thread view lightweight; fetch
`/emails/{id}` or `/sent-emails/{id}` for a single message's
full content.
properties:
id:
type: string
format: uuid
subject:
type:
- string
- "null"
description: Normalized subject of the thread (Re/Fwd prefixes stripped).
root_message_id:
type:
- string
- "null"
description: Message-ID of the conversation root, when known.
message_count:
type: integer
description: |
Total messages in the thread. `messages` is capped (most
recent first, then re-sorted oldest-first), so
`message_count > messages.length` signals truncation.
first_message_at:
type:
- string
- "null"
format: date-time
last_message_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
messages:
type: array
items:
type: object
description: One message in a thread (inbound or outbound).
properties:
direction:
type: string
enum:
- inbound
- outbound
description: |
`inbound` for a received email (`/emails/{id}`), `outbound`
for a send (`/sent-emails/{id}`). Use it with `id` to fetch
full content from the right endpoint.
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
from:
type:
- string
- "null"
to:
type:
- string
- "null"
subject:
type:
- string
- "null"
status:
type:
- string
- "null"
description: Lifecycle status (an EmailStatus or SentEmailStatus value, per `direction`).
timestamp:
type:
- string
- "null"
format: date-time
description: received_at for inbound, created_at for outbound.
required:
- direction
- id
required:
- id
- message_count
- created_at
- messages
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/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:
* `pending` â deploy in flight; the runtime has not yet
confirmed the new bundle is live.
* `deployed` â the running edge handler is the latest code.
* `failed` â the most recent deploy attempt failed; the
previously-live code (if any) is still running. The
`deploy_error` field carries the error message.
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:
* `pending` â deploy in flight; the runtime has not yet
confirmed the new bundle is live.
* `deployed` â the running edge handler is the latest code.
* `failed` â the most recent deploy attempt failed; the
previously-live code (if any) is still running. The
`deploy_error` field carries the error message.
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:
* `pending` â deploy in flight; the runtime has not yet
confirmed the new bundle is live.
* `deployed` â the running edge handler is the latest code.
* `failed` â the most recent deploy attempt failed; the
previously-live code (if any) is still running. The
`deploy_error` field carries the error message.
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:
* `pending` â deploy in flight; the runtime has not yet
confirmed the new bundle is live.
* `deployed` â the running edge handler is the latest code.
* `failed` â the most recent deploy attempt failed; the
previously-live code (if any) is still running. The
`deploy_error` field carries the error message.
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+@` 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 function is currently `deployed`. Returns 422
if the function is in `pending` or `failed` state, or if the
org has no verified inbound domain to receive the test mail.
Returns 400 if `local_part` is set to a value that does not
match the local-part character set.
tags:
- Functions
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
local_part:
type: string
description: |
Override the synthetic local-part. When set, the
test email is sent to `@`
instead of the default
`__primitive_function_test+@`.
Must start with an alphanumeric and contain only
letters, digits, dots, plus signs, hyphens, or
underscores; 1-64 characters total.
minLength: 1
maxLength: 64
pattern: ^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$
example: summarize
responses:
"200":
description: Test send queued
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
Metadata returned by POST /functions/{id}/test. The send is
queued; poll `trace_url` to watch the run progress through
send -> inbound -> webhook deliveries -> outbound requests,
logs, and replies.
properties:
test_run_id:
type: string
format: uuid
description: Durable test run id used to fetch the run trace.
inbound_domain:
type: string
description: Verified inbound domain the test email was sent to.
to:
type: string
description: Synthetic local-part plus inbound_domain. Visible in the org's inbox.
from:
type: string
description: Primitive-controlled outbound sender used for the test.
send_id:
type: string
description: |
Outbound message id from the underlying send. NOT the
inbound email's id; the inbound id is created when the
email arrives via MX and lands on the function's
invocations list.
subject:
type: string
description: Subject placed on the test email so it can be located in the inbox.
poll_since:
type: string
format: date-time
description: |
ISO timestamp suitable as a `since` lower bound when
polling /emails for the inbound's arrival. Captured
slightly before the send to absorb light clock skew.
watch_url:
type: string
format: uri
description: Function detail page where invocations show up live.
trace_url:
type: string
description: Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace.
required:
- test_run_id
- inbound_domain
- to
- from
- send_id
- subject
- poll_since
- watch_url
- trace_url
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: Function not in a state that can be invoked, or no inbound domain configured
"502":
$ref: "#/components/responses/BadGateway"
description: Primitive could not complete the downstream SMTP request
"503":
$ref: "#/components/responses/ServiceUnavailable"
description: Sending agent misconfigured
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}/test-runs/{run_id}/trace:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
- name: run_id
in: path
required: true
description: Function test run id returned by POST /functions/{id}/test.
schema:
type: string
format: uuid
get:
operationId: getFunctionTestRunTrace
summary: Get a function test run trace
description: |
Returns the current end-to-end trace for a function test run.
The trace is intentionally partial while the test is still in
flight: callers can poll this endpoint and watch it fill in
from send -> inbound -> webhook deliveries -> outbound
requests, logs, and replies.
tags:
- Functions
responses:
"200":
description: Function test run trace
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
End-to-end trace for a `POST /functions/{id}/test` run. The
shape is stable, but many nested sections are null or empty
until the corresponding phase has happened.
properties:
state:
type: string
description: |
High-level state for a function test run trace:
- `send_failed`: the initial test email send failed.
- `waiting_for_send`: the test run was created but no send result has been recorded yet.
- `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.
- `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.
- `completed`: the function webhook completed successfully.
- `failed`: webhook delivery exhausted retries.
enum:
- send_failed
- waiting_for_send
- waiting_for_inbound
- waiting_for_function
- completed
- failed
test_run:
type: object
properties:
id:
type: string
format: uuid
function_id:
type: string
format: uuid
inbound_domain:
type: string
to:
type: string
from:
type: string
subject:
type: string
poll_since:
type: string
format: date-time
created_at:
type: string
format: date-time
sent_at:
type:
- string
- "null"
format: date-time
send_error:
type:
- string
- "null"
required:
- id
- function_id
- inbound_domain
- to
- from
- subject
- poll_since
- created_at
- sent_at
- send_error
test_send:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
queue_id:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- status
- queue_id
- created_at
- updated_at
inbound_email:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
status:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
received_at:
type: string
format: date-time
from:
type: string
to:
type: string
subject:
type:
- string
- "null"
webhook_status:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
webhook_attempt_count:
type: integer
webhook_last_status_code:
type:
- integer
- "null"
webhook_last_error:
type:
- string
- "null"
required:
- id
- status
- received_at
- from
- to
- subject
- webhook_status
- webhook_attempt_count
- webhook_last_status_code
- webhook_last_error
deliveries:
type: array
items:
type: object
properties:
id:
type: string
description: Webhook delivery id.
endpoint_id:
type: string
format: uuid
endpoint_url:
type: string
format: uri
status:
type: string
enum:
- pending
- delivered
- header_confirmed
- failed
attempt_count:
type: integer
duration_ms:
type:
- integer
- "null"
last_error:
type:
- string
- "null"
last_error_code:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
endpoint:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
kind:
type: string
description: Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear.
function_id:
type:
- string
- "null"
format: uuid
function_name:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
enabled:
type: boolean
deactivated_at:
type:
- string
- "null"
format: date-time
is_current_function:
type: boolean
required:
- id
- kind
- function_id
- function_name
- domain_id
- enabled
- deactivated_at
- is_current_function
required:
- id
- endpoint_id
- endpoint_url
- status
- attempt_count
- duration_ms
- last_error
- last_error_code
- created_at
- updated_at
- endpoint
outbound_requests:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
function_id:
type: string
format: uuid
webhook_delivery_id:
type:
- string
- "null"
email_id:
type:
- string
- "null"
format: uuid
endpoint_id:
type:
- string
- "null"
format: uuid
method:
type: string
url:
type: string
format: uri
host:
type: string
path:
type: string
status_code:
type:
- integer
- "null"
ok:
type:
- boolean
- "null"
duration_ms:
type: integer
error:
type:
- string
- "null"
ts:
type: string
format: date-time
required:
- id
- function_id
- webhook_delivery_id
- email_id
- endpoint_id
- method
- url
- host
- path
- status_code
- ok
- duration_ms
- error
- ts
logs:
type: array
items:
type: object
description: |
One row from GET /functions/{id}/logs. Represents a single
captured log line emitted by the running handler (e.g. via
`console.log` / `console.error`).
properties:
id:
type: string
format: uuid
description: Unique log row id (stable across pages).
function_id:
type: string
format: uuid
description: The function this log row belongs to.
level:
type: string
enum:
- debug
- log
- info
- warn
- error
description: |
Severity. `log` is the runtime's default for unannotated
`console.log` calls; the other levels match standard
`console.*` methods.
message:
type: string
description: |
The textual message body. The runtime stringifies non-string
arguments before persisting, so this is always a plain
string.
ts:
type: string
format: date-time
description: |
When the handler emitted this line. Newest-first ordering
on this column drives pagination; clock is the runtime's,
not the gateway's.
metadata:
type:
- object
- "null"
additionalProperties: true
description: |
Optional structured payload the runtime attaches alongside
the message (e.g. extra args passed to `console.log`).
Shape is opaque; treat keys as untyped.
required:
- id
- function_id
- level
- message
- ts
replies:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
to:
type: string
subject:
type: string
queue_id:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- status
- to
- subject
- queue_id
- created_at
required:
- state
- test_run
- test_send
- inbound_email
- deliveries
- outbound_requests
- logs
- replies
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/functions/routing-topology:
get:
operationId: getOrgRoutingTopology
summary: Get the org's function routing topology
description: |
Returns a single snapshot of how inbound mail is routed across
this org's active domains and functions: which active domain has
which function bound, the org's fallback function (if any), and
every deployed function with no route bound. Use this to answer
"which of my functions actually receive mail?" diagnostically.
tags:
- Functions
responses:
"200":
description: Routing topology
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
Org-wide map of function routing: which domain points at which
function, the org's fallback binding (if any), and every
deployed function with no route currently bound.
properties:
domains:
type: array
items:
type: object
properties:
domain_id:
type: string
format: uuid
domain:
type: string
routed_function:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type: string
required:
- id
- name
endpoint_enabled:
type:
- boolean
- "null"
required:
- domain_id
- domain
- routed_function
- endpoint_enabled
fallback_function:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type: string
required:
- id
- name
fallback_enabled:
type:
- boolean
- "null"
unrouted_functions:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
required:
- id
- name
required:
- domains
- fallback_function
- fallback_enabled
- unrouted_functions
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
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
security:
- BearerAuth: []
/functions/{id}/routing:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: getFunctionRouting
summary: Get a function's current route binding
description: |
Returns the endpoint binding for the function, or null when no
route is currently bound. The binding identifies whether the
function receives mail for a specific domain (scoped) or for any
active domain that has no scoped binding (fallback).
tags:
- Functions
responses:
"200":
description: Function routing
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
oneOf:
- type: object
description: |
A single route binding for a function. `domain` is null when the
binding is the org's fallback (any active domain without a scoped
binding); otherwise it carries the scoped domain. `rules` is
reserved for future routing predicates.
properties:
endpoint_id:
type: string
format: uuid
enabled:
type: boolean
domain:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type:
- string
- "null"
required:
- id
rules:
type: object
description: Future routing predicates. Currently empty.
delivery_count:
type: integer
success_count:
type: integer
failure_count:
type: integer
consecutive_fails:
type: integer
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
required:
- endpoint_id
- enabled
- domain
- rules
- type: "null"
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: []
/functions/{id}/route:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
put:
operationId: setFunctionRoute
summary: Bind a route to a function
description: |
Binds inbound mail to this function. The route target is either
a specific verified domain (scoped) or the org's fallback (any
active domain with no scoped binding). If another function is
already bound at the target, returns a `conflict` envelope
describing the holder; re-issue with `takeover: true` to
deactivate that prior binding and install this one.
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
description: |
Target for a route binding. Either a specific verified domain
(scoped) or the org-wide fallback. Pass `takeover: true` to
deactivate any conflicting binding before installing this one.
properties:
target:
oneOf:
- type: object
properties:
kind:
type: string
enum:
- domain
domainId:
type: string
format: uuid
required:
- kind
- domainId
- type: object
properties:
kind:
type: string
enum:
- fallback
required:
- kind
takeover:
type: boolean
description: When true, deactivate any conflicting binding before installing this one.
required:
- target
responses:
"200":
description: Route bound, or conflict requiring takeover
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
On success, carries the new `routing`. On conflict, carries
`conflict` describing the binding holder so the caller can
re-issue with `takeover: true`.
properties:
routing:
oneOf:
- type: object
description: |
A single route binding for a function. `domain` is null when the
binding is the org's fallback (any active domain without a scoped
binding); otherwise it carries the scoped domain. `rules` is
reserved for future routing predicates.
properties:
endpoint_id:
type: string
format: uuid
enabled:
type: boolean
domain:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type:
- string
- "null"
required:
- id
rules:
type: object
description: Future routing predicates. Currently empty.
delivery_count:
type: integer
success_count:
type: integer
failure_count:
type: integer
consecutive_fails:
type: integer
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
required:
- endpoint_id
- enabled
- domain
- rules
- type: "null"
conflict:
type: object
properties:
kind:
type: string
enum:
- http
- function
functionId:
type:
- string
- "null"
format: uuid
functionName:
type:
- string
- "null"
url:
type:
- string
- "null"
required:
- kind
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: unsetFunctionRoute
summary: Unbind any route from a function
description: |
Deactivates every active endpoint bound to this function. The
function stays deployed but stops receiving inbound mail. Safe
to call when no route is currently bound (no-op).
tags:
- Functions
responses:
"200":
description: Route unbound
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
unrouted:
type: boolean
enum:
- true
required:
- unrouted
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: []
/functions/{id}/secrets:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: listFunctionSecrets
summary: List a function's secrets
description: |
Returns metadata for every secret bound to the function, with
managed entries (provisioned by Primitive) listed first and
user-set entries listed alphabetically after. **Values are
never returned.** Secret writes are write-only.
Managed entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,
`PRIMITIVE_API_KEY`, `PRIMITIVE_API_BASE_URL`) carry a
`description` instead of `created_at` / `updated_at`. They
cannot be created, updated, or deleted via this API.
tags:
- Functions
responses:
"200":
description: List of secrets (metadata only, no values)
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
items:
type: array
items:
type: object
description: |
One row from GET /functions/{id}/secrets. Discriminate on the
`managed` field:
* `managed = true` â system secret provisioned by Primitive.
`description` is set; `created_at` / `updated_at` are
null because the row is virtual (resolved at deploy time
from the managed registry, not stored in the secrets
table).
* `managed = false` â secret the user set via the API.
`created_at` / `updated_at` are set; `description` is
null.
properties:
key:
type: string
managed:
type: boolean
description: True for managed system secrets, false for user-set entries.
description:
type:
- string
- "null"
description: Set on managed entries only; null on user-set entries.
created_at:
type:
- string
- "null"
format: date-time
description: Set on user-set entries only; null on managed entries.
updated_at:
type:
- string
- "null"
format: date-time
description: Set on user-set entries only; null on managed entries.
required:
- key
- managed
required:
- items
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: []
post:
operationId: createFunctionSecret
summary: Create or update a secret
description: |
Idempotent insert-or-update keyed on `(function_id, key)`.
Returns 201 the first time the key is set, 200 on subsequent
updates. Values are encrypted at rest and only become visible
to the running handler on the next deploy (`PUT /functions/{id}`
with the existing code is sufficient to refresh bindings).
Keys must match `^[A-Z_][A-Z0-9_]*$` (uppercase letters,
digits, underscores; first character is a letter or
underscore). Values are at most 4096 UTF-8 bytes. System-
managed keys are reserved and rejected.
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: Body for POST /functions/{id}/secrets.
properties:
key:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
description: |
Uppercase letters, digits, and underscores. Must start with
a letter or underscore. System-managed keys (e.g.
PRIMITIVE_WEBHOOK_SECRET, PRIMITIVE_API_KEY, and
PRIMITIVE_API_BASE_URL) are reserved.
value:
type: string
minLength: 1
maxLength: 4096
description: |
Secret value, up to 4096 UTF-8 bytes. Encrypted at rest.
Never returned by any read endpoint.
required:
- key
- value
responses:
"200":
description: Secret updated
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 and PUT secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
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
"201":
description: Secret created
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 and PUT secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
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}/secrets/{key}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
- name: key
in: path
required: true
description: Secret key. Must match `^[A-Z_][A-Z0-9_]*$`.
schema:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
put:
operationId: setFunctionSecret
summary: Set a secret by key
description: |
Path-keyed companion to `POST /functions/{id}/secrets`.
Idempotent: returns 201 the first time the key is set, 200 on
subsequent updates. Same validation rules and same write-only
guarantees as the POST verb; the new value lands in the running
handler on the next deploy.
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: Body for PUT /functions/{id}/secrets/{key}. Key comes from the path.
properties:
value:
type: string
minLength: 1
maxLength: 4096
required:
- value
responses:
"200":
description: Secret updated
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 and PUT secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
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
"201":
description: Secret created
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 and PUT secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
delete:
operationId: deleteFunctionSecret
summary: Delete a secret
description: |
Removes the secret. The binding stays live in the running
handler until the next deploy refreshes the binding set
(`PUT /functions/{id}` with the existing code is sufficient).
Returns 404 if the key did not exist. Managed system keys
cannot be deleted.
tags:
- Functions
responses:
"204":
description: Secret deleted
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/org/secrets:
get:
operationId: listOrgSecrets
summary: List org-level (global) secrets
description: |
Returns metadata for every org-level secret. Org secrets apply
to every function in the org and are read as `env.` in
handlers. **Values are never returned.** Secret writes are
write-only. A function-level secret of the same name overrides
the org-level value for that function.
tags:
- Functions
responses:
"200":
description: List of org secrets (metadata only, no values)
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
items:
type: array
items:
type: object
description: |
One row from GET /org/secrets. Org secrets are always user-set
(there are no managed org secrets), so `created_at` /
`updated_at` are always present.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- key
- created_at
- updated_at
required:
- items
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: createOrgSecret
summary: Create or update an org secret
description: |
Idempotent insert-or-update keyed on `(org_id, key)`. Returns
201 the first time the key is set, 200 on subsequent updates.
Values are encrypted at rest. A changed value lands in a
function only on that function's next deploy.
Keys must match `^[A-Z_][A-Z0-9_]*$` (uppercase letters,
digits, underscores; first character is a letter or
underscore). Values are at most 4096 UTF-8 bytes. System-
managed keys are reserved and rejected.
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: Body for POST /org/secrets.
properties:
key:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
description: |
Uppercase letters, digits, and underscores. Must start with
a letter or underscore. System-managed keys are reserved.
value:
type: string
minLength: 1
maxLength: 4096
description: |
Secret value, up to 4096 UTF-8 bytes. Encrypted at rest.
Never returned by any read endpoint.
required:
- key
- value
responses:
"200":
description: Secret updated
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 and PUT org secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
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
"201":
description: Secret created
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 and PUT org secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
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
/org/secrets/{key}:
parameters:
- name: key
in: path
required: true
description: Secret key. Must match `^[A-Z_][A-Z0-9_]*$`.
schema:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
put:
operationId: setOrgSecret
summary: Set an org secret by key
description: |
Path-keyed companion to `POST /org/secrets`. Idempotent:
returns 201 the first time the key is set, 200 on subsequent
updates. Same validation and write-only guarantees as POST.
tags:
- Functions
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: Body for PUT /org/secrets/{key}. Key comes from the path.
properties:
value:
type: string
minLength: 1
maxLength: 4096
required:
- value
responses:
"200":
description: Secret updated
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 and PUT org secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
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
"201":
description: Secret created
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 and PUT org secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
security:
- BearerAuth: []
delete:
operationId: deleteOrgSecret
summary: Delete an org secret
description: |
Removes the org secret. Functions keep the previous value until
each is redeployed. Returns 404 if the key did not exist.
tags:
- Functions
responses:
"204":
description: Secret deleted
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/functions/{id}/logs:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
get:
operationId: listFunctionLogs
summary: List a function's execution logs
description: |
Returns the most recent `function_logs` rows for the function,
newest first. Each row is a single `console.log` / `console.error`
invocation captured from the running handler.
Page through history with the opaque `cursor` returned as
`next_cursor`; pass it back as the `cursor` query param on the
next call. `next_cursor` is `null` when there are no further
rows. The cursor format is an implementation detail and should
not be parsed by callers.
tags:
- Functions
parameters:
- name: limit
in: query
required: false
description: |
Maximum number of rows to return. Clamped to 1..200; default
50.
schema:
type: integer
minimum: 1
maximum: 200
default: 50
- name: cursor
in: query
required: false
description: |
Opaque pagination cursor from a previous response's
`next_cursor`. Omit on the first call.
schema:
type: string
responses:
"200":
description: List of log rows (newest first) plus pagination cursor.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
items:
type: array
items:
type: object
description: |
One row from GET /functions/{id}/logs. Represents a single
captured log line emitted by the running handler (e.g. via
`console.log` / `console.error`).
properties:
id:
type: string
format: uuid
description: Unique log row id (stable across pages).
function_id:
type: string
format: uuid
description: The function this log row belongs to.
level:
type: string
enum:
- debug
- log
- info
- warn
- error
description: |
Severity. `log` is the runtime's default for unannotated
`console.log` calls; the other levels match standard
`console.*` methods.
message:
type: string
description: |
The textual message body. The runtime stringifies non-string
arguments before persisting, so this is always a plain
string.
ts:
type: string
format: date-time
description: |
When the handler emitted this line. Newest-first ordering
on this column drives pagination; clock is the runtime's,
not the gateway's.
metadata:
type:
- object
- "null"
additionalProperties: true
description: |
Optional structured payload the runtime attaches alongside
the message (e.g. extra args passed to `console.log`).
Shape is opaque; treat keys as untyped.
required:
- id
- function_id
- level
- message
- ts
next_cursor:
type:
- string
- "null"
description: |
Pass back as `cursor` to fetch the next
page. `null` when no further rows exist.
required:
- items
- next_cursor
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
security:
- BearerAuth: []
/memories:
put:
operationId: setMemory
summary: Set a memory
description: Create or update a durable JSON memory under an org or function scope. Function-authenticated requests use their own Function id and cannot override it. If no explicit scope is provided for other credentials, `x-primitive-function-id` is used next, and other requests default to org scope. Function scope uses the function id UUID, not the function name.
tags:
- Memories
security:
- BearerAuth: []
parameters:
- name: x-primitive-function-id
in: header
required: false
description: Optional function id UUID used as the default scope for non-function-authenticated requests when the body does not include `scope`. Ignored when `scope` is provided.
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
value:
description: JSON value accepted by Primitive Memories. The server accepts strings, numbers, booleans, null, arrays, and objects, validates nested values, and rejects values that do not serialize as JSON.
oneOf:
- type: "null"
- type: string
- type: number
- type: boolean
- type: array
items:
$ref: "#/components/schemas/MemoryJsonValue"
- type: object
additionalProperties:
$ref: "#/components/schemas/MemoryJsonValue"
scope:
description: Memory scope. `org` resolves to the authenticated organization. `function` requires the function id UUID in `id`; function names are not valid scope identifiers. Function-authenticated requests cannot override their own Function scope.
oneOf:
- type: object
additionalProperties: false
properties:
type:
type: string
enum:
- org
required:
- type
- type: object
additionalProperties: false
properties:
type:
type: string
enum:
- function
id:
type: string
format: uuid
description: Function id UUID.
required:
- type
- id
ttl_seconds:
type: integer
minimum: 1
maximum: 31536000
description: Set or replace the TTL in seconds. Mutually exclusive with `expires_at` and `clear_ttl`.
expires_at:
type: string
format: date-time
description: Set or replace the absolute expiration timestamp. Mutually exclusive with `ttl_seconds` and `clear_ttl`.
clear_ttl:
type: boolean
description: Clear any existing TTL. Mutually exclusive with `ttl_seconds` and `expires_at`.
if_absent:
type: boolean
description: Create only when the key is absent. Mutually exclusive with `if_version`.
if_version:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
required:
- key
- value
responses:
"200":
description: Existing memory updated.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
additionalProperties: false
description: Memory record returned by get and set operations.
properties:
id:
type: string
format: uuid
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
scope:
type: object
additionalProperties: false
description: Resolved memory scope returned by the API.
properties:
type:
type: string
enum:
- org
- function
id:
type: string
format: uuid
description: Org id for org scope, function id for function scope.
required:
- type
- id
value:
description: JSON value accepted by Primitive Memories. The server accepts strings, numbers, booleans, null, arrays, and objects, validates nested values, and rejects values that do not serialize as JSON.
oneOf:
- type: "null"
- type: string
- type: number
- type: boolean
- type: array
items:
$ref: "#/components/schemas/MemoryJsonValue"
- type: object
additionalProperties:
$ref: "#/components/schemas/MemoryJsonValue"
version:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
last_read_at:
type:
- string
- "null"
format: date-time
description: Last successful get timestamp, or null before any get.
read_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
write_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
expires_at:
type:
- string
- "null"
format: date-time
description: Expiration timestamp, or null for no TTL.
created_by:
type:
- string
- "null"
description: Actor that created the memory, when available.
updated_by:
type:
- string
- "null"
description: Actor that last updated the memory, when available.
required:
- id
- key
- scope
- value
- version
- created_at
- updated_at
- last_read_at
- read_count
- write_count
- expires_at
- created_by
- updated_by
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
"201":
description: Memory created.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
additionalProperties: false
description: Memory record returned by get and set operations.
properties:
id:
type: string
format: uuid
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
scope:
type: object
additionalProperties: false
description: Resolved memory scope returned by the API.
properties:
type:
type: string
enum:
- org
- function
id:
type: string
format: uuid
description: Org id for org scope, function id for function scope.
required:
- type
- id
value:
description: JSON value accepted by Primitive Memories. The server accepts strings, numbers, booleans, null, arrays, and objects, validates nested values, and rejects values that do not serialize as JSON.
oneOf:
- type: "null"
- type: string
- type: number
- type: boolean
- type: array
items:
$ref: "#/components/schemas/MemoryJsonValue"
- type: object
additionalProperties:
$ref: "#/components/schemas/MemoryJsonValue"
version:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
last_read_at:
type:
- string
- "null"
format: date-time
description: Last successful get timestamp, or null before any get.
read_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
write_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
expires_at:
type:
- string
- "null"
format: date-time
description: Expiration timestamp, or null for no TTL.
created_by:
type:
- string
- "null"
description: Actor that created the memory, when available.
updated_by:
type:
- string
- "null"
description: Actor that last updated the memory, when available.
required:
- id
- key
- scope
- value
- version
- created_at
- updated_at
- last_read_at
- read_count
- write_count
- expires_at
- created_by
- updated_by
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"402":
$ref: "#/components/responses/PaymentRequired"
description: Usage credits are exhausted or payment is required.
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
get:
operationId: getMemory
summary: Get a memory
description: Fetch one active memory by key and scope. Omit scope parameters to use automatic scope resolution. Function-authenticated requests use their own Function id and cannot override it. Function scope uses a function id UUID in `scope_id`.
tags:
- Memories
security:
- BearerAuth: []
parameters:
- name: key
in: query
required: true
description: Memory key. Must be at most 512 UTF-8 bytes.
schema:
type: string
minLength: 1
maxLength: 512
- name: scope_type
in: query
required: false
description: Explicit scope type. Omit to use automatic scope resolution. Pass `function` with `scope_id=`, or `org` with no `scope_id`. Function-authenticated requests cannot override their automatic Function scope.
schema:
type: string
enum:
- org
- function
- name: scope_id
in: query
required: false
description: Function id UUID when `scope_type=function`. Not valid with `scope_type=org`. Function-authenticated requests may only use their own Function id.
schema:
type: string
format: uuid
- name: x-primitive-function-id
in: header
required: false
description: Optional function id UUID used as the default scope for non-function-authenticated requests when query scope parameters are omitted.
schema:
type: string
format: uuid
responses:
"200":
description: Memory record with value.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
additionalProperties: false
description: Memory record returned by get and set operations.
properties:
id:
type: string
format: uuid
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
scope:
type: object
additionalProperties: false
description: Resolved memory scope returned by the API.
properties:
type:
type: string
enum:
- org
- function
id:
type: string
format: uuid
description: Org id for org scope, function id for function scope.
required:
- type
- id
value:
description: JSON value accepted by Primitive Memories. The server accepts strings, numbers, booleans, null, arrays, and objects, validates nested values, and rejects values that do not serialize as JSON.
oneOf:
- type: "null"
- type: string
- type: number
- type: boolean
- type: array
items:
$ref: "#/components/schemas/MemoryJsonValue"
- type: object
additionalProperties:
$ref: "#/components/schemas/MemoryJsonValue"
version:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
last_read_at:
type:
- string
- "null"
format: date-time
description: Last successful get timestamp, or null before any get.
read_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
write_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
expires_at:
type:
- string
- "null"
format: date-time
description: Expiration timestamp, or null for no TTL.
created_by:
type:
- string
- "null"
description: Actor that created the memory, when available.
updated_by:
type:
- string
- "null"
description: Actor that last updated the memory, when available.
required:
- id
- key
- scope
- value
- version
- created_at
- updated_at
- last_read_at
- read_count
- write_count
- expires_at
- created_by
- updated_by
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"402":
$ref: "#/components/responses/PaymentRequired"
description: Usage credits are exhausted or payment is required.
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
delete:
operationId: deleteMemory
summary: Delete a memory
description: Delete one active memory by key and scope. Function-authenticated requests use their own Function id and cannot override it. Deletes are idempotent without `if_version`; a stale `if_version` returns `memory_conflict`.
tags:
- Memories
security:
- BearerAuth: []
parameters:
- name: key
in: query
required: true
description: Memory key. Must be at most 512 UTF-8 bytes.
schema:
type: string
minLength: 1
maxLength: 512
- name: scope_type
in: query
required: false
description: Explicit scope type. Omit to use automatic scope resolution. Pass `function` with `scope_id=`, or `org` with no `scope_id`. Function-authenticated requests cannot override their automatic Function scope.
schema:
type: string
enum:
- org
- function
- name: scope_id
in: query
required: false
description: Function id UUID when `scope_type=function`. Not valid with `scope_type=org`. Function-authenticated requests may only use their own Function id.
schema:
type: string
format: uuid
- name: if_version
in: query
required: false
description: Optional compare-and-delete version.
schema:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
- name: x-primitive-function-id
in: header
required: false
description: Optional function id UUID used as the default scope for non-function-authenticated requests when query scope parameters are omitted.
schema:
type: string
format: uuid
responses:
"200":
description: Delete result.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
additionalProperties: false
properties:
deleted:
type: boolean
key:
type: string
scope:
type: object
additionalProperties: false
description: Resolved memory scope returned by the API.
properties:
type:
type: string
enum:
- org
- function
id:
type: string
format: uuid
description: Org id for org scope, function id for function scope.
required:
- type
- id
required:
- deleted
- key
- scope
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"402":
$ref: "#/components/responses/PaymentRequired"
description: Usage credits are exhausted or payment is required.
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/memories/search:
get:
operationId: searchMemories
summary: Search memories
description: List active memories in a scope by lexicographic key prefix. Results are ordered by key ascending. This is prefix search, not free-text search. Function-authenticated requests use their own Function id and cannot override it. Pass `include_value=false` to return metadata only.
tags:
- Memories
security:
- BearerAuth: []
parameters:
- name: prefix
in: query
required: false
description: Key prefix to match. Empty string lists all active memories in the selected scope.
schema:
type: string
maxLength: 512
default: ""
- name: cursor
in: query
required: false
description: Key cursor from a previous response meta.cursor.
schema:
type: string
minLength: 1
maxLength: 512
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
description: Number of results per page
- name: include_value
in: query
required: false
description: Pass `false` to omit the `value` field and return metadata only.
schema:
type: string
enum:
- "true"
- "false"
default: "true"
- name: updated_after
in: query
required: false
description: Only include memories updated at or after this timestamp.
schema:
type: string
format: date-time
- name: updated_before
in: query
required: false
description: Only include memories updated at or before this timestamp.
schema:
type: string
format: date-time
- name: scope_type
in: query
required: false
description: Explicit scope type. Omit to use automatic scope resolution. Pass `function` with `scope_id=`, or `org` with no `scope_id`. Function-authenticated requests cannot override their automatic Function scope.
schema:
type: string
enum:
- org
- function
- name: scope_id
in: query
required: false
description: Function id UUID when `scope_type=function`. Not valid with `scope_type=org`. Function-authenticated requests may only use their own Function id.
schema:
type: string
format: uuid
- name: x-primitive-function-id
in: header
required: false
description: Optional function id UUID used as the default scope for non-function-authenticated requests when query scope parameters are omitted.
schema:
type: string
format: uuid
responses:
"200":
description: Paginated memory records.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
meta:
type: object
properties:
total:
type: integer
description: Total number of matching records
limit:
type: integer
description: Page size used for this request
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if no more results
required:
- total
- limit
- cursor
required:
- success
- data
- meta
- type: object
properties:
data:
type: array
items:
type: object
additionalProperties: false
description: Metadata for a Primitive memory. Search responses omit `value` when `include_value=false`.
properties:
id:
type: string
format: uuid
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
scope:
type: object
additionalProperties: false
description: Resolved memory scope returned by the API.
properties:
type:
type: string
enum:
- org
- function
id:
type: string
format: uuid
description: Org id for org scope, function id for function scope.
required:
- type
- id
value:
description: JSON value accepted by Primitive Memories. The server accepts strings, numbers, booleans, null, arrays, and objects, validates nested values, and rejects values that do not serialize as JSON.
oneOf:
- type: "null"
- type: string
- type: number
- type: boolean
- type: array
items:
$ref: "#/components/schemas/MemoryJsonValue"
- type: object
additionalProperties:
$ref: "#/components/schemas/MemoryJsonValue"
version:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
last_read_at:
type:
- string
- "null"
format: date-time
description: Last successful get timestamp, or null before any get.
read_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
write_count:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
expires_at:
type:
- string
- "null"
format: date-time
description: Expiration timestamp, or null for no TTL.
created_by:
type:
- string
- "null"
description: Actor that created the memory, when available.
updated_by:
type:
- string
- "null"
description: Actor that last updated the memory, when available.
required:
- id
- key
- scope
- version
- created_at
- updated_at
- last_read_at
- read_count
- write_count
- expires_at
- created_by
- updated_by
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"402":
$ref: "#/components/responses/PaymentRequired"
description: Usage credits are exhausted or payment is required.
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/x402/payout-addresses:
post:
operationId: registerPayoutAddress
summary: Register a payout address
description: |
Register (or update) the default payout address your org receives x402
payments at, for a given network. You prove control of the address with
an org-bound `personal_sign` signature over the message produced by the
SDK helper `buildPayoutRegistrationMessage`. The org id is taken from your
authenticated key, never the body, so a captured signature can't register
an address under another org. Exactly one default address exists per
(org, network); registering again replaces it. A payee MUST register a
payout address before calling `createChallenge`, because the challenge's
`pay_to` is resolved from this directory.
tags:
- Payments
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
address:
type: string
pattern: ^0x[0-9a-fA-F]{40}$
description: The payout address (your signer's own EVM address), 0x-prefixed.
network:
type: string
enum:
- base
- base-sepolia
description: The chain the address receives on.
signature:
type: string
pattern: ^0x[0-9a-fA-F]+$
description: |
A `personal_sign` signature over the org-bound message produced by
the SDK helper `buildPayoutRegistrationMessage`. Recovered and
checked against `address`; the org id is bound into the signed bytes.
issued_at:
type: string
format: date-time
description: |
ISO-8601 timestamp embedded in the signed message. Must be within a
short freshness window (about 10 minutes) of server time.
label:
type: string
maxLength: 80
description: Optional human-readable label.
required:
- address
- network
- signature
- issued_at
responses:
"201":
description: Payout address registered (or updated) and set as default
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
address:
type: string
description: The checksummed payout address.
network:
type: string
enum:
- base
- base-sepolia
label:
type:
- string
- "null"
is_default:
type: boolean
description: Exactly one address per (org, network) is the default.
verified_at:
type: string
format: date-time
description: When ownership of the address was last proven.
created_at:
type: string
format: date-time
required:
- id
- address
- network
- label
- is_default
- verified_at
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: |
The request was well-formed but could not be processed. For Payments
this covers a missing payout address, a failed payment verification, a
spend-policy decline, or an expired challenge; `error.code` distinguishes
them.
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
get:
operationId: listPayoutAddresses
summary: List payout addresses
description: List your org's registered payout addresses, newest first.
tags:
- Payments
security:
- BearerAuth: []
responses:
"200":
description: Your registered payout addresses
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
properties:
id:
type: string
format: uuid
address:
type: string
description: The checksummed payout address.
network:
type: string
enum:
- base
- base-sepolia
label:
type:
- string
- "null"
is_default:
type: boolean
description: Exactly one address per (org, network) is the default.
verified_at:
type: string
format: date-time
description: When ownership of the address was last proven.
created_at:
type: string
format: date-time
required:
- id
- address
- network
- label
- is_default
- verified_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
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/x402/email-challenges:
post:
operationId: createEmailChallenge
summary: Create an email-native payment challenge
description: |
Issue an x402 payment challenge over a real email thread (the payee
side). Unlike `createChallenge` (which mints a synthetic challenge id),
this sends the challenge as an email from `from` to `to` and binds the
payment to that DKIM-authenticated thread. The `pay_to` address and the
token asset are resolved server-side from your registered default payout
address for the network, never from the request. The response carries
the thread's `interaction_id` plus the `challenge` (the
`payment_requirements`, the `nonce_binding`, and `expires_at`) the payer
needs to sign; the payer replies with a signed `payment` interaction
step. Amounts are in token base units (USDC has 6 decimals, so `"10000"`
is 0.01 USDC).
tags:
- Payments
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
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: |
Issue a payment challenge over an email thread. `from` is your sending
address (the funds receiver; ownership is enforced at send, exactly as
for outbound mail) and `to` is the payer's address. The `pay_to` payout
wallet and the token asset are resolved server-side, never taken from
the request.
properties:
from:
type: string
format: email
description: |
Your sending address (the payee / funds receiver). Must be an
address your org is allowed to send from.
to:
type: string
format: email
description: The payer's email address the challenge is sent to.
amount:
type: string
pattern: ^[1-9][0-9]{0,38}$
description: |
Amount to collect, in token base units (unlike the `charge` CLI
command, which also accepts `--amount-usdc`, this field takes base
units only). USDC has 6 decimals, so `"10000"` is 0.01 USDC:
multiply a human USDC amount by 1,000,000 (0.01 USDC -> `"10000"`).
network:
type: string
enum:
- base
- base-sepolia
expires_in:
type: integer
minimum: 60
maximum: 86400
description: Seconds until the challenge expires. Defaults to 300.
resource:
type: string
format: uri
maxLength: 2048
description: Optional URL identifying what is being paid for.
description:
type: string
maxLength: 512
description: Optional human-readable description of the payment.
required:
- from
- to
- amount
- network
responses:
"200":
description: |
Idempotent replay: a request with a previously-used idempotency key
returns the original issued challenge without sending a second email.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
The result of issuing an email-native payment challenge. `interaction_id`
is the real email thread id (`uuid@domain`) the payment is bound to;
`challenge_id` is the underlying challenge record. Hand the `challenge`
to the payer, who replies with a signed `payment` interaction step (the
SDK `payEmailChallenge` helper builds it).
properties:
interaction_id:
type: string
description: The email thread id (`uuid@domain`) the payment is bound to.
challenge_id:
type: string
format: uuid
description: The underlying challenge record id.
challenge:
type: object
description: |
The challenge the payer needs to sign and pay, carried inside an
email-native challenge response.
properties:
payment_requirements:
type: object
description: |
The x402 `PaymentRequirements` the payer signs over. Field names are
x402's native camelCase, preserved byte-for-byte.
properties:
scheme:
type: string
description: The x402 settlement scheme. Always `exact` for v1.
example: exact
network:
type: string
enum:
- base
- base-sepolia
maxAmountRequired:
type: string
description: Amount in token base units.
payTo:
type: string
description: The payee's resolved payout address (checksummed).
asset:
type: string
description: The token contract address (checksummed). USDC.
resource:
type: string
description:
type: string
maxTimeoutSeconds:
type: integer
extra:
type: object
description: |
The token's load-bearing EIP-712 domain params. `name` differs by
chain (Base mainnet USDC is `USD Coin`, Base Sepolia is `USDC`); a
wrong value produces a signature the verifier rejects.
properties:
name:
type: string
version:
type: string
required:
- name
- version
required:
- scheme
- network
- maxAmountRequired
- payTo
- asset
- extra
nonce_binding:
type: object
description: |
The interaction binding the payer hashes into the EIP-3009 nonce
(`deriveEip3009Nonce`). Pinning the nonce to this binding is what lets an
x402 payment ride asynchronous transports safely: a replayed challenge
can't redirect funds and a signed payment can't settle twice.
properties:
interaction_id:
type: string
description: Interaction id, including its `@domain` part.
challenge_step_id:
type: string
format: uuid
challenge_nonce:
type: string
pattern: ^[0-9a-f]{64}$
description: 32 random bytes as 64 lowercase hex chars.
required:
- interaction_id
- challenge_step_id
- challenge_nonce
expires_at:
type: string
format: date-time
description: ISO-8601 expiry of the challenge.
required:
- payment_requirements
- nonce_binding
- expires_at
required:
- interaction_id
- challenge_id
- challenge
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
"201":
description: Email challenge issued
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
The result of issuing an email-native payment challenge. `interaction_id`
is the real email thread id (`uuid@domain`) the payment is bound to;
`challenge_id` is the underlying challenge record. Hand the `challenge`
to the payer, who replies with a signed `payment` interaction step (the
SDK `payEmailChallenge` helper builds it).
properties:
interaction_id:
type: string
description: The email thread id (`uuid@domain`) the payment is bound to.
challenge_id:
type: string
format: uuid
description: The underlying challenge record id.
challenge:
type: object
description: |
The challenge the payer needs to sign and pay, carried inside an
email-native challenge response.
properties:
payment_requirements:
type: object
description: |
The x402 `PaymentRequirements` the payer signs over. Field names are
x402's native camelCase, preserved byte-for-byte.
properties:
scheme:
type: string
description: The x402 settlement scheme. Always `exact` for v1.
example: exact
network:
type: string
enum:
- base
- base-sepolia
maxAmountRequired:
type: string
description: Amount in token base units.
payTo:
type: string
description: The payee's resolved payout address (checksummed).
asset:
type: string
description: The token contract address (checksummed). USDC.
resource:
type: string
description:
type: string
maxTimeoutSeconds:
type: integer
extra:
type: object
description: |
The token's load-bearing EIP-712 domain params. `name` differs by
chain (Base mainnet USDC is `USD Coin`, Base Sepolia is `USDC`); a
wrong value produces a signature the verifier rejects.
properties:
name:
type: string
version:
type: string
required:
- name
- version
required:
- scheme
- network
- maxAmountRequired
- payTo
- asset
- extra
nonce_binding:
type: object
description: |
The interaction binding the payer hashes into the EIP-3009 nonce
(`deriveEip3009Nonce`). Pinning the nonce to this binding is what lets an
x402 payment ride asynchronous transports safely: a replayed challenge
can't redirect funds and a signed payment can't settle twice.
properties:
interaction_id:
type: string
description: Interaction id, including its `@domain` part.
challenge_step_id:
type: string
format: uuid
challenge_nonce:
type: string
pattern: ^[0-9a-f]{64}$
description: 32 random bytes as 64 lowercase hex chars.
required:
- interaction_id
- challenge_step_id
- challenge_nonce
expires_at:
type: string
format: date-time
description: ISO-8601 expiry of the challenge.
required:
- payment_requirements
- nonce_binding
- expires_at
required:
- interaction_id
- challenge_id
- challenge
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: |
The request was well-formed but could not be processed. For Payments
this covers a missing payout address, a failed payment verification, a
spend-policy decline, or an expired challenge; `error.code` distinguishes
them.
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/x402/challenges:
post:
operationId: createChallenge
summary: Create a payment challenge
description: |
Create an x402 payment challenge (the payee side of a payment). The
`pay_to` address is resolved server-side from your registered default
payout address for the network, never from the request. The response
carries the `nonce_binding` and `payment_requirements` the payer needs to
sign; hand the whole challenge object to the payer (for example in an
email reply). Amounts are in token base units (USDC has 6 decimals, so
`"10000"` is 0.01 USDC).
tags:
- Payments
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
amount:
type: string
pattern: ^[1-9][0-9]{0,38}$
description: |
Amount to collect, in token base units. USDC has 6 decimals, so
`"10000"` is 0.01 USDC.
network:
type: string
enum:
- base
- base-sepolia
payer_org:
type: string
format: uuid
description: |
The org id allowed to pay this challenge (on-net binding). Optional.
expires_in:
type: integer
minimum: 60
maximum: 86400
description: Seconds until the challenge expires. Defaults to 3600.
resource:
type: string
format: uri
maxLength: 2048
description: |
Optional URL identifying what is being paid for. Defaults to a
synthetic `x402:challenge:` identifier.
description:
type: string
maxLength: 512
description: Optional human-readable description of the payment.
required:
- amount
- network
responses:
"201":
description: Challenge created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- pending
- settling
- settled
- failed
- expired
network:
type: string
enum:
- base
- base-sepolia
asset:
type: string
description: Token contract address (checksummed).
amount:
type: string
description: Amount in token base units.
pay_to:
type: string
description: The payee's resolved payout address (checksummed).
payer_org:
type:
- string
- "null"
description: The org id bound as payer, if one was set at creation.
resource:
type:
- string
- "null"
description:
type:
- string
- "null"
nonce_binding:
type: object
description: |
The interaction binding the payer hashes into the EIP-3009 nonce
(`deriveEip3009Nonce`). Pinning the nonce to this binding is what lets an
x402 payment ride asynchronous transports safely: a replayed challenge
can't redirect funds and a signed payment can't settle twice.
properties:
interaction_id:
type: string
description: Interaction id, including its `@domain` part.
challenge_step_id:
type: string
format: uuid
challenge_nonce:
type: string
pattern: ^[0-9a-f]{64}$
description: 32 random bytes as 64 lowercase hex chars.
required:
- interaction_id
- challenge_step_id
- challenge_nonce
settle_tx:
type:
- string
- "null"
description: On-chain settlement transaction hash once settled.
settled_at:
type:
- string
- "null"
format: date-time
failure_reason:
type:
- string
- "null"
expires_at:
type: string
format: date-time
created_at:
type: string
format: date-time
payment_requirements:
description: |
Present on the create response. Hand the whole challenge (including
this) to the payer; `getChallenge` omits it (it is for status polling
by the challenger).
allOf:
- type: object
description: |
The x402 `PaymentRequirements` the payer signs over. Field names are
x402's native camelCase, preserved byte-for-byte.
properties:
scheme:
type: string
description: The x402 settlement scheme. Always `exact` for v1.
example: exact
network:
type: string
enum:
- base
- base-sepolia
maxAmountRequired:
type: string
description: Amount in token base units.
payTo:
type: string
description: The payee's resolved payout address (checksummed).
asset:
type: string
description: The token contract address (checksummed). USDC.
resource:
type: string
description:
type: string
maxTimeoutSeconds:
type: integer
extra:
type: object
description: |
The token's load-bearing EIP-712 domain params. `name` differs by
chain (Base mainnet USDC is `USD Coin`, Base Sepolia is `USDC`); a
wrong value produces a signature the verifier rejects.
properties:
name:
type: string
version:
type: string
required:
- name
- version
required:
- scheme
- network
- maxAmountRequired
- payTo
- asset
- extra
required:
- id
- status
- network
- asset
- amount
- pay_to
- nonce_binding
- expires_at
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: |
The request was well-formed but could not be processed. For Payments
this covers a missing payout address, a failed payment verification, a
spend-policy decline, or an expired challenge; `error.code` distinguishes
them.
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/x402/challenges/{id}:
get:
operationId: getChallenge
summary: Get a payment challenge
description: |
Fetch a challenge you created, to poll its `status` and settlement
receipt (`settle_tx`). Scoped to the challenger org that created it.
tags:
- Payments
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
responses:
"200":
description: The challenge
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- pending
- settling
- settled
- failed
- expired
network:
type: string
enum:
- base
- base-sepolia
asset:
type: string
description: Token contract address (checksummed).
amount:
type: string
description: Amount in token base units.
pay_to:
type: string
description: The payee's resolved payout address (checksummed).
payer_org:
type:
- string
- "null"
description: The org id bound as payer, if one was set at creation.
resource:
type:
- string
- "null"
description:
type:
- string
- "null"
nonce_binding:
type: object
description: |
The interaction binding the payer hashes into the EIP-3009 nonce
(`deriveEip3009Nonce`). Pinning the nonce to this binding is what lets an
x402 payment ride asynchronous transports safely: a replayed challenge
can't redirect funds and a signed payment can't settle twice.
properties:
interaction_id:
type: string
description: Interaction id, including its `@domain` part.
challenge_step_id:
type: string
format: uuid
challenge_nonce:
type: string
pattern: ^[0-9a-f]{64}$
description: 32 random bytes as 64 lowercase hex chars.
required:
- interaction_id
- challenge_step_id
- challenge_nonce
settle_tx:
type:
- string
- "null"
description: On-chain settlement transaction hash once settled.
settled_at:
type:
- string
- "null"
format: date-time
failure_reason:
type:
- string
- "null"
expires_at:
type: string
format: date-time
created_at:
type: string
format: date-time
payment_requirements:
description: |
Present on the create response. Hand the whole challenge (including
this) to the payer; `getChallenge` omits it (it is for status polling
by the challenger).
allOf:
- type: object
description: |
The x402 `PaymentRequirements` the payer signs over. Field names are
x402's native camelCase, preserved byte-for-byte.
properties:
scheme:
type: string
description: The x402 settlement scheme. Always `exact` for v1.
example: exact
network:
type: string
enum:
- base
- base-sepolia
maxAmountRequired:
type: string
description: Amount in token base units.
payTo:
type: string
description: The payee's resolved payout address (checksummed).
asset:
type: string
description: The token contract address (checksummed). USDC.
resource:
type: string
description:
type: string
maxTimeoutSeconds:
type: integer
extra:
type: object
description: |
The token's load-bearing EIP-712 domain params. `name` differs by
chain (Base mainnet USDC is `USD Coin`, Base Sepolia is `USDC`); a
wrong value produces a signature the verifier rejects.
properties:
name:
type: string
version:
type: string
required:
- name
- version
required:
- scheme
- network
- maxAmountRequired
- payTo
- asset
- extra
required:
- id
- status
- network
- asset
- amount
- pay_to
- nonce_binding
- expires_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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/x402/challenges/{id}/pay:
post:
operationId: payChallenge
summary: Pay a payment challenge
description: |
Settle a challenge addressed to your org as payer. The request body
carries a signed x402 `PaymentPayload`: an EIP-3009
`transferWithAuthorization` signed locally with your own key, whose nonce
is bound to the challenge via the SDK's `deriveEip3009Nonce`. The platform
verifies every signed field against its own record of the challenge,
applies your spend policy, and settles on-chain through a facilitator.
Settlement is non-custodial; Primitive never holds funds. Idempotent:
paying an already-settled challenge returns the original receipt. Most
callers use the SDK `pay()` helper rather than building the payload by
hand.
tags:
- Payments
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
- 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
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
payment:
type: object
description: |
A signed x402 v1 `PaymentPayload`. The SDK `pay()` helper builds this;
callers rarely construct it by hand. Field names are x402-native.
properties:
x402Version:
type: integer
const: 1
scheme:
type: string
const: exact
network:
type: string
enum:
- base
- base-sepolia
payload:
type: object
properties:
signature:
type: string
pattern: ^0x[0-9a-fA-F]+$
description: The EIP-712 signature over the authorization.
authorization:
type: object
description: The EIP-3009 `transferWithAuthorization` fields, as strings.
properties:
from:
type: string
to:
type: string
value:
type: string
validAfter:
type: string
validBefore:
type: string
nonce:
type: string
required:
- from
- to
- value
- validAfter
- validBefore
- nonce
required:
- signature
- authorization
required:
- x402Version
- scheme
- network
- payload
required:
- payment
responses:
"200":
description: Challenge settled (or already settled)
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- settled
settle_tx:
type:
- string
- "null"
description: On-chain settlement transaction hash.
required:
- id
- status
- settle_tx
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: |
The request was well-formed but could not be processed. For Payments
this covers a missing payout address, a failed payment verification, a
spend-policy decline, or an expired challenge; `error.code` distinguishes
them.
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
"502":
$ref: "#/components/responses/BadGateway"
description: Primitive could not complete the downstream SMTP request
/x402/spend-policy:
get:
operationId: getSpendPolicy
summary: Get your spend policy
description: |
Read your org's outbound spend policy: the kill-switch, per-payment and
per-day caps, and the payee allowlist. Returns the defaults (no limits,
not paused) when no policy has been set.
tags:
- Payments
security:
- BearerAuth: []
responses:
"200":
description: The spend policy
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
The payer's outbound spend policy. Returned with defaults (not paused,
no caps, any on-net payee) when none is set.
properties:
paused:
type: boolean
description: Kill-switch. When true, all outbound payments are refused.
max_per_payment:
type:
- string
- "null"
description: Per-payment cap in token base units, or null for no cap.
max_per_day:
type:
- string
- "null"
description: Rolling-day cap in token base units, or null for no cap.
allowlist:
type:
- array
- "null"
items:
type: string
format: uuid
description: |
Allowed payee org ids. `null` allows any on-net payee; `[]` denies
all.
required:
- paused
- max_per_payment
- max_per_day
- allowlist
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
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
put:
operationId: updateSpendPolicy
summary: Update your spend policy
description: |
Update your org's spend policy. Applied as a merge: only the fields you
include change, and omitted fields keep their current value, so a partial
update can't silently reset the kill-switch. Send an explicit `null` to
clear a cap. Caps are in token base units.
tags:
- Payments
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
description: |
Merge update: only the fields you include change; omit a field to keep
its current value; send `null` to clear a cap.
properties:
paused:
type: boolean
max_per_payment:
type:
- string
- "null"
pattern: ^[1-9][0-9]{0,38}$
max_per_day:
type:
- string
- "null"
pattern: ^[1-9][0-9]{0,38}$
allowlist:
type:
- array
- "null"
maxItems: 1000
items:
type: string
format: uuid
responses:
"200":
description: The updated spend policy
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
description: |
The payer's outbound spend policy. Returned with defaults (not paused,
no caps, any on-net payee) when none is set.
properties:
paused:
type: boolean
description: Kill-switch. When true, all outbound payments are refused.
max_per_payment:
type:
- string
- "null"
description: Per-payment cap in token base units, or null for no cap.
max_per_day:
type:
- string
- "null"
description: Rolling-day cap in token base units, or null for no cap.
allowlist:
type:
- array
- "null"
items:
type: string
format: uuid
description: |
Allowed payee org ids. `null` allows any on-net payee; `[]` denies
all.
required:
- paused
- max_per_payment
- max_per_day
- allowlist
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/x402/declined-payments:
get:
operationId: listDeclinedPayments
summary: List declined payments
description: |
The 50 most recent payments your org's spend policy declined, newest
first. Use this to see why an outbound payment was refused (a cap, the
payee allowlist, or the kill-switch) instead of only reading the
dashboard.
tags:
- Payments
security:
- BearerAuth: []
responses:
"200":
description: Recently declined payments
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: A payment the org's spend policy refused.
properties:
id:
type: string
format: uuid
challenge_id:
type:
- string
- "null"
format: uuid
description: The challenge that was declined, if still present.
counterparty_org:
type:
- string
- "null"
format: uuid
description: The payee (challenger) org, when known.
network:
type: string
enum:
- base
- base-sepolia
amount:
type: string
description: Amount in token base units.
reason:
type: string
description: Why the payment was declined (cap, allowlist, paused).
declined_at:
type: string
format: date-time
required:
- id
- network
- amount
- reason
- declined_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
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/registries:
get:
operationId: listRegistries
summary: List the registries you own
tags:
- Registries
responses:
"200":
description: Registries owned by the caller
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
name:
type: string
description:
type:
- string
- "null"
is_public:
type: boolean
publish_policy:
type: string
enum:
- owner_only
- request
- open
description: |
Who may publish into a registry. owner_only: only the registry owner.
request: anyone may request and the owner approves. open: anyone may
publish and it lists immediately (no approval step).
required:
- id
- slug
- name
- description
- is_public
- publish_policy
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: []
description: List the registries you own
post:
operationId: createRegistry
summary: Create a registry
tags:
- Registries
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
slug:
type: string
pattern: ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$
description: Lowercase slug, unique across registries.
name:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
is_public:
type: boolean
publish_policy:
type: string
enum:
- owner_only
- request
- open
description: |
Who may publish into a registry. owner_only: only the registry owner.
request: anyone may request and the owner approves. open: anyone may
publish and it lists immediately (no approval step).
required:
- slug
- name
responses:
"201":
description: Registry created
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
required:
- id
- slug
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: Invalid request parameters
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
description: Create a registry
/registries/{slug}:
parameters:
- name: slug
in: path
required: true
schema:
type: string
description: The registry slug
get:
operationId: getRegistry
summary: Get a public registry's metadata
tags:
- Registries
security: []
responses:
"200":
description: Registry metadata
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
name:
type: string
description:
type:
- string
- "null"
is_public:
type: boolean
publish_policy:
type: string
enum:
- owner_only
- request
- open
description: |
Who may publish into a registry. owner_only: only the registry owner.
request: anyone may request and the owner approves. open: anyone may
publish and it lists immediately (no approval step).
required:
- id
- slug
- name
- description
- is_public
- publish_policy
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
description: Get a public registry's metadata
patch:
operationId: updateRegistry
summary: Update a registry you own
tags:
- Registries
requestBody:
required: true
content:
application/json:
schema:
type: object
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
is_public:
type: boolean
publish_policy:
type: string
enum:
- owner_only
- request
- open
description: |
Who may publish into a registry. owner_only: only the registry owner.
request: anyone may request and the owner approves. open: anyone may
publish and it lists immediately (no approval step).
responses:
"200":
description: Registry updated
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
id:
type: string
format: uuid
required:
- id
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
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: Invalid request parameters
security:
- BearerAuth: []
description: Update a registry you own
delete:
operationId: deleteRegistry
summary: Delete a registry you own
description: Removes the registry from discovery and frees its slug for re-creation.
tags:
- Registries
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
security:
- BearerAuth: []
/registries/{slug}/agents:
parameters:
- name: slug
in: path
required: true
schema:
type: string
description: The registry slug.
get:
operationId: listRegistryAgents
summary: List agents in a registry
tags:
- Registries
security: []
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
description: Maximum number of items to return (1-200).
- name: cursor
in: query
required: false
schema:
type: string
description: The address of the last agent from the previous page.
responses:
"200":
description: Approved, reachable agents. Empty for an unknown or private registry.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
meta:
type: object
properties:
total:
type: integer
description: Total number of matching records
limit:
type: integer
description: Page size used for this request
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if no more results
required:
- total
- limit
- cursor
required:
- success
- data
- meta
- properties:
data:
type: array
items:
type: object
description: An agent's public directory profile.
properties:
address:
type: string
display_name:
type: string
title:
type:
- string
- "null"
description:
type:
- string
- "null"
tags:
type: array
items:
type: string
handle:
type:
- string
- "null"
description: The registry-scoped name. Null on the global by-address read.
last_reachable_at:
type:
- string
- "null"
format: date-time
description: When the agent's address was last confirmed to still route to its endpoint. A freshness signal for ranking listings; null until the first check.
required:
- address
- display_name
- title
- description
- tags
- handle
- last_reachable_at
description: List agents in a registry
post:
operationId: publishAgent
summary: Publish an agent into a registry
tags:
- Registries
requestBody:
required: true
content:
application/json:
schema:
type: object
description: Publish an agent into a registry. When display_name is present the agent identity is defined (create-or-get by address) in the same call before publishing; omit the define fields to publish an already-defined agent.
properties:
address:
type: string
handle:
type: string
description: The registry-scoped name to list the agent under.
display_name:
type: string
minLength: 1
maxLength: 120
description: Present to define the agent identity before publishing (define + publish in one call).
endpoint_id:
type: string
format: uuid
description: Optional, only used when defining. Omit to resolve the endpoint from the address's routing.
title:
type:
- string
- "null"
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
tags:
type: array
items:
type: string
maxLength: 40
maxItems: 20
required:
- address
- handle
dependentRequired:
endpoint_id:
- display_name
title:
- display_name
description:
- display_name
tags:
- display_name
responses:
"200":
description: Idempotent replay of an existing publication
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
status:
type: string
enum:
- approved
- requested
description: approved lists immediately; requested pends owner approval.
handle:
type: string
idempotent_replay:
type: boolean
description: True when the publish matched an existing identical publication.
required:
- status
- handle
- idempotent_replay
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
"201":
description: Published (approved) or request created (requested)
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
status:
type: string
enum:
- approved
- requested
description: approved lists immediately; requested pends owner approval.
handle:
type: string
idempotent_replay:
type: boolean
description: True when the publish matched an existing identical publication.
required:
- status
- handle
- idempotent_replay
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"403":
$ref: "#/components/responses/Forbidden"
description: Authenticated caller lacks permission for the operation
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: Invalid request parameters
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
description: Publish an agent into a registry
/registries/{slug}/agents/{handle}:
parameters:
- name: slug
in: path
required: true
schema:
type: string
description: The registry slug.
- name: handle
in: path
required: true
schema:
type: string
description: The registry-scoped handle the agent is published under.
get:
operationId: resolveRegistryHandle
summary: Resolve a registry handle to its agent
tags:
- Registries
security: []
responses:
"200":
description: The agent listing under the handle
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
description: An agent's public directory profile.
properties:
address:
type: string
display_name:
type: string
title:
type:
- string
- "null"
description:
type:
- string
- "null"
tags:
type: array
items:
type: string
handle:
type:
- string
- "null"
description: The registry-scoped name. Null on the global by-address read.
last_reachable_at:
type:
- string
- "null"
format: date-time
description: When the agent's address was last confirmed to still route to its endpoint. A freshness signal for ranking listings; null until the first check.
required:
- address
- display_name
- title
- description
- tags
- handle
- last_reachable_at
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
description: Resolve a registry handle to its agent
delete:
operationId: unpublishAgent
summary: Unpublish an agent from a registry
tags:
- Registries
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
security:
- BearerAuth: []
description: Unpublish an agent from a registry
/registries/{slug}/requests:
parameters:
- name: slug
in: path
required: true
schema:
type: string
description: The registry slug.
get:
operationId: listRegistryRequests
summary: List pending publication requests
tags:
- Registries
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
description: Maximum number of items to return (1-200).
responses:
"200":
description: Pending requests for a registry you own
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: array
items:
type: object
description: A pending publication request, as the registry owner sees it.
properties:
id:
type: string
format: uuid
address:
type: string
display_name:
type: string
handle:
type:
- string
- "null"
requested_at:
type: string
format: date-time
required:
- id
- address
- display_name
- handle
- requested_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: []
description: List pending publication requests
/registries/{slug}/requests/{id}:
parameters:
- name: slug
in: path
required: true
schema:
type: string
description: The registry slug.
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
post:
operationId: decideRegistryRequest
summary: Approve or reject a publication request
tags:
- Registries
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
decision:
type: string
enum:
- approved
- rejected
required:
- decision
responses:
"200":
description: Decision applied
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
status:
type: string
enum:
- approved
- rejected
required:
- status
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
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: Invalid request parameters
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
description: Approve or reject a publication request
/agents:
post:
operationId: defineAgent
summary: Define an agent identity
tags:
- Registries
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
address:
type: string
description: The agent's globally unique email address; mail to it must route to an endpoint the account controls.
endpoint_id:
type: string
format: uuid
description: Optional. The endpoint the agent runs on. Omit it to resolve the endpoint from the address's routing automatically; supply it to pin a specific endpoint, which is then validated against the address's route.
display_name:
type: string
minLength: 1
maxLength: 120
title:
type:
- string
- "null"
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
tags:
type: array
items:
type: string
maxLength: 40
maxItems: 20
required:
- address
- display_name
responses:
"201":
description: Agent defined
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
properties:
id:
type: string
format: uuid
address:
type: string
required:
- id
- address
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"409":
$ref: "#/components/responses/Conflict"
description: The request conflicts with the current state of the resource
"422":
$ref: "#/components/responses/UnprocessableEntity"
description: Invalid request parameters
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
description: Define an agent identity
/agents/{address}:
parameters:
- name: address
in: path
required: true
schema:
type: string
description: The agent's email address (URL-encoded).
get:
operationId: getAgent
summary: Get an agent's public profile by address
tags:
- Registries
security: []
responses:
"200":
description: The agent's public profile
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- properties:
data:
type: object
description: An agent's public directory profile.
properties:
address:
type: string
display_name:
type: string
title:
type:
- string
- "null"
description:
type:
- string
- "null"
tags:
type: array
items:
type: string
handle:
type:
- string
- "null"
description: The registry-scoped name. Null on the global by-address read.
last_reachable_at:
type:
- string
- "null"
format: date-time
description: When the agent's address was last confirmed to still route to its endpoint. A freshness signal for ranking listings; null until the first check.
required:
- address
- display_name
- title
- description
- tags
- handle
- last_reachable_at
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
description: Get an agent's public profile by address
/templates:
get:
operationId: listTemplates
summary: List function templates
description: List approved Function templates available for browsing and installation. Results are cacheable and paginated with `data.next_cursor`.
tags:
- Templates
security: []
parameters:
- name: q
in: query
required: false
description: Search templates by title, summary, or slug.
schema:
type: string
minLength: 1
- name: tag
in: query
required: false
description: Filter templates by an exact tag.
schema:
type: string
minLength: 1
- name: cursor
in: query
required: false
description: Cursor from a previous response `data.next_cursor` value.
schema:
type: string
- name: limit
in: query
required: false
description: Maximum number of templates to return. The server caps this at 100.
schema:
type: integer
minimum: 1
maximum: 100
default: 50
responses:
"200":
description: Paginated template registry summaries.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
items:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Stable template slug used in template URLs and install commands.
title:
type: string
summary:
type: string
author:
type: object
additionalProperties: false
properties:
id:
type: string
minLength: 1
name:
type: string
minLength: 1
url:
type: string
format: uri
required:
- id
- name
tags:
type: array
items:
type: string
verified:
type: boolean
install_count:
type: integer
minimum: 0
github_repo:
type: string
pattern: ^[A-Za-z0-9-]+/[A-Za-z0-9_.-]+$
description: GitHub repository in owner/repo form.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- slug
- title
- summary
- author
- tags
- verified
- install_count
- github_repo
- created_at
- updated_at
next_cursor:
type:
- string
- "null"
description: Cursor to pass as the next `cursor` query value, or null when there are no more templates.
required:
- items
- next_cursor
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/templates/{id}:
get:
operationId: getTemplate
summary: Get a function template
description: Fetch one approved Function template by slug, including its manifest snapshot and README. The stored source files used for install are not returned.
tags:
- Templates
security: []
parameters:
- name: id
in: path
required: true
description: Template slug from the template manifest.
schema:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Template slug.
responses:
"200":
description: Template registry detail.
content:
application/json:
schema:
allOf:
- type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
- type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Stable template slug used in template URLs and install commands.
title:
type: string
summary:
type: string
author:
type: object
additionalProperties: false
properties:
id:
type: string
minLength: 1
name:
type: string
minLength: 1
url:
type: string
format: uri
required:
- id
- name
tags:
type: array
items:
type: string
verified:
type: boolean
install_count:
type: integer
minimum: 0
github_repo:
type: string
pattern: ^[A-Za-z0-9-]+/[A-Za-z0-9_.-]+$
description: GitHub repository in owner/repo form.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
description:
type:
- string
- "null"
github_sha:
type: string
github_path:
type:
- string
- "null"
manifest:
type: object
additionalProperties: false
properties:
schemaVersion:
type: integer
const: 1
id:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Stable template slug from the manifest.
title:
type: string
minLength: 1
summary:
type: string
minLength: 1
description:
type: string
author:
type: object
additionalProperties: false
properties:
id:
type: string
minLength: 1
name:
type: string
minLength: 1
url:
type: string
format: uri
required:
- id
- name
tags:
type: array
items:
type: string
minLength: 1
default: []
source:
oneOf:
- type: object
additionalProperties: false
properties:
mode:
type: string
const: managed-build
dir:
type: string
minLength: 1
default: .
required:
- mode
- dir
- type: object
additionalProperties: false
properties:
mode:
type: string
const: bundle
file:
type: string
minLength: 1
required:
- mode
- file
install:
type: object
additionalProperties: false
properties:
mode:
type: string
enum:
- deploy
- scaffold
editFiles:
type: array
items:
type: string
minLength: 1
default: []
reason:
type: string
default: ""
required:
- mode
- editFiles
- reason
secrets:
type: array
items:
type: object
additionalProperties: false
properties:
key:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
required:
type: boolean
default: true
description:
type: string
required:
- key
- required
default: []
secretGroups:
type: array
items:
type: object
additionalProperties: false
properties:
keys:
type: array
minItems: 2
items:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
min:
type: integer
minimum: 1
default: 1
description:
type: string
required:
- keys
- min
default: []
variables:
type: array
items:
type: object
additionalProperties: false
properties:
key:
type: string
minLength: 1
prompt:
type: string
minLength: 1
default:
type: string
file:
type: string
minLength: 1
type:
type: string
enum:
- string
- select
- url
- email
default: string
options:
type: array
items:
type: string
minLength: 1
validation:
type: object
additionalProperties: false
properties:
pattern:
type: string
minLength: 1
maxLength:
type: integer
minimum: 1
required:
- key
- prompt
- type
default: []
consumesVendors:
type: array
items:
type: object
additionalProperties: false
properties:
slug:
type: string
minLength: 1
required:
type: boolean
default: true
required:
- slug
default: []
setup:
type: object
additionalProperties: false
properties:
agent:
type: string
minLength: 1
prompt:
type: string
minLength: 1
produces:
type: array
items:
type: string
minLength: 1
default: []
required:
- agent
- prompt
- produces
postInstall:
type: string
required:
- schemaVersion
- id
- title
- summary
- author
- tags
- source
- install
- secrets
- secretGroups
- variables
readme:
type:
- string
- "null"
status:
type: string
enum:
- pending
- approved
- rejected
required:
- id
- slug
- title
- summary
- author
- tags
- verified
- install_count
- github_repo
- created_at
- updated_at
- description
- github_sha
- github_path
- manifest
- readme
- status
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"404":
$ref: "#/components/responses/NotFound"
description: Resource not found
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
/send-mail/demo:
post:
operationId: sendMailDemo
summary: Try send-mail without authentication (simulation)
description: "Unauthenticated sandbox for `POST /send-mail`. Accepts the same request body, validates it identically, and returns a realistic synthetic success envelope with `demo: true`. **No email is sent, queued, or stored.** Use this to exercise the send flow without credentials; to send real mail, sign up at https://primitive.dev/signup for an API key and call `POST /send-mail` with `Authorization: Bearer prim_`."
tags:
- Sending
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
from:
type: string
minLength: 3
maxLength: 998
description: RFC 5322 From header. The sender domain must be a verified outbound domain for your organization.
to:
type: string
minLength: 3
maxLength: 320
description: Recipient address. Recipient eligibility depends on your account's outbound entitlements.
subject:
type: string
minLength: 1
maxLength: 998
description: Subject line for the outbound message
body_text:
type: string
description: Plain-text message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
body_html:
type: string
description: HTML message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
in_reply_to:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
description: Message-ID of the direct parent email when sending a threaded reply.
references:
type: array
maxItems: 100
description: Full ordered message-id chain for the thread.
items:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
attachments:
type: array
maxItems: 100
description: Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.
items:
type: object
additionalProperties: false
properties:
filename:
type: string
minLength: 1
maxLength: 255
description: Attachment filename. Control characters are rejected.
content_type:
type: string
minLength: 1
maxLength: 255
description: Optional MIME content type. Control characters are rejected.
content_base64:
type: string
minLength: 1
maxLength: 44040192
description: Base64-encoded attachment bytes.
required:
- filename
- content_base64
wait:
type: boolean
description: When true, wait for the first downstream SMTP delivery outcome before returning.
wait_timeout_ms:
type: integer
minimum: 1000
maximum: 30000
description: Maximum time to wait for a delivery outcome when wait is true. Defaults to 30000.
required:
- from
- to
- subject
example:
from: [email protected]
to: [email protected]
subject: Hello from my agent
body_text: This is a test send via the public demo endpoint.
responses:
"200":
description: Simulated send accepted. Identical shape to a fresh asynchronous /send-mail response, plus a top-level `demo` flag. No mail was sent.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
demo:
type: boolean
const: true
note:
type: string
description: Human-readable reminder that the response is simulated.
data:
type: object
properties:
id:
type: string
description: Persisted sent-email attempt ID.
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
from:
type: string
description: |
Bare from-address actually written on the wire. Echoed
on every success branch so callers can confirm what
went out, particularly useful for the /emails/{id}/reply
path where `from` is server-derived from the inbound's
recipient when the caller doesn't override.
For sends where the caller passed a from-header that
included a display name (e.g. `"Acme Support" `),
this field is the parsed bare address (`[email protected]`).
The display name was sent on the wire intact; this field
just makes the address easy to compare against allowlists.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's OUTBOUND relay
(the box that signs your mail and submits it to the
receiving MTA). NOT the receiver's queue id.
The receiver may also report its own queue id in
`smtp_response_text` (e.g. `"250 2.0.0 Ok: queued as
99D111927CDA"` from a Postfix receiver). Those two ids
refer to different mail systems and are NOT comparable.
Treat `queue_id` as Primitive-internal and the
receiver's id as remote-system-internal.
Null on rows that never reached the relay (queued,
gate_denied, agent_failed before signing).
accepted:
type: array
items:
type: string
description: Recipient addresses accepted by the relay.
rejected:
type: array
items:
type: string
description: Recipient addresses rejected by the relay.
client_idempotency_key:
type: string
description: Effective idempotency key used for this send.
request_id:
type: string
description: Server-issued request identifier for support and tracing.
content_hash:
type: string
description: Stable hash of the canonical send payload.
delivery_status:
type: string
description: |
Narrower enum covering only the four terminal delivery
outcomes returned to a synchronous `wait: true` send.
On the SendMailResult shape, `delivery_status` is always
equal to `status` whenever both are present (i.e. on
terminal-state replays and live wait=true responses).
The two fields exist so callers that want to type-narrow
on "this is a delivery outcome" can pattern-match against
the four-value enum without handling the broader
SentEmailStatus value set (which also covers `queued`,
`submitted_to_agent`, `agent_failed`, `gate_denied`,
`unknown`).
On async-mode and pre-terminal responses, `delivery_status`
is absent and only `status` is populated. Use `status` if
you want a single field that's always present.
enum:
- delivered
- bounced
- deferred
- wait_timeout
smtp_response_code:
type:
- integer
- "null"
description: SMTP response code from the first downstream delivery outcome when wait is true.
smtp_response_text:
type: string
description: SMTP response text from the first downstream delivery outcome when wait is true.
idempotent_replay:
type: boolean
description: |
True when the response replays a previously-recorded send
keyed by `client_idempotency_key` (same key, same canonical
payload). False on a fresh send and on gate-denied
responses. Lets callers branch on cache state without
diffing fields.
required:
- id
- status
- from
- queue_id
- accepted
- rejected
- client_idempotency_key
- request_id
- content_hash
- idempotent_replay
required:
- success
- demo
- data
example:
success: true
demo: true
note: "Simulated response. No email was sent. Sign up at https://primitive.dev/signup for an API key, then POST the same body to /v1/send-mail with Authorization: Bearer prim_."
data:
id: 3f1c0a9e-2b7d-4e5a-9c6f-8d2e1a4b5c6d
status: queued
from: [email protected]
queue_id: demo_9a3f1c0a
accepted:
- [email protected]
rejected: []
client_idempotency_key: null
request_id: 7b2e4d6a-1c3f-4a5b-8d9e-0f1a2b3c4d5e
content_hash: a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
idempotent_replay: false
dedup_reason: null
"400":
$ref: "#/components/responses/ValidationError"
description: Invalid request parameters
"413":
$ref: "#/components/responses/PayloadTooLarge"
description: Request body exceeds the demo size cap.
"415":
$ref: "#/components/responses/UnsupportedMediaType"
description: Content-Type must be application/json.
"429":
$ref: "#/components/responses/RateLimited"
description: Demo rate limit reached.
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
/send-mail/batch:
post:
operationId: sendMailBatch
summary: Send a batch of emails in one request
description: "Send up to 100 independent messages in a single call. Each message uses the same body schema and the same send path as `POST /send-mail`, so auth, validation, rate limiting, and idempotency apply per message. Messages are processed independently: one failing does not fail the others â every message gets an entry in `results` carrying either `data` (on success) or `error` (on failure), with the original `index`."
tags:
- Sending
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- messages
properties:
messages:
type: array
minItems: 1
maxItems: 100
description: The messages to send. Each item is a standard send-mail request body.
items:
type: object
additionalProperties: false
properties:
from:
type: string
minLength: 3
maxLength: 998
description: RFC 5322 From header. The sender domain must be a verified outbound domain for your organization.
to:
type: string
minLength: 3
maxLength: 320
description: Recipient address. Recipient eligibility depends on your account's outbound entitlements.
subject:
type: string
minLength: 1
maxLength: 998
description: Subject line for the outbound message
body_text:
type: string
description: Plain-text message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
body_html:
type: string
description: HTML message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
in_reply_to:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
description: Message-ID of the direct parent email when sending a threaded reply.
references:
type: array
maxItems: 100
description: Full ordered message-id chain for the thread.
items:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
attachments:
type: array
maxItems: 100
description: Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.
items:
type: object
additionalProperties: false
properties:
filename:
type: string
minLength: 1
maxLength: 255
description: Attachment filename. Control characters are rejected.
content_type:
type: string
minLength: 1
maxLength: 255
description: Optional MIME content type. Control characters are rejected.
content_base64:
type: string
minLength: 1
maxLength: 44040192
description: Base64-encoded attachment bytes.
required:
- filename
- content_base64
wait:
type: boolean
description: When true, wait for the first downstream SMTP delivery outcome before returning.
wait_timeout_ms:
type: integer
minimum: 1000
maximum: 30000
description: Maximum time to wait for a delivery outcome when wait is true. Defaults to 30000.
required:
- from
- to
- subject
example:
messages:
- from: [email protected]
to: [email protected]
subject: Hi A
body_text: Hello A
- from: [email protected]
to: [email protected]
subject: Hi B
body_text: Hello B
responses:
"200":
description: Per-message results. The request as a whole returns HTTP 200 even when individual messages fail; inspect each `results` entry.
content:
application/json:
schema:
type: object
required:
- success
- data
properties:
success:
type: boolean
data:
type: object
required:
- count
- results
properties:
count:
type: integer
description: Number of messages in the batch.
results:
type: array
items:
type: object
required:
- index
- success
properties:
index:
type: integer
description: Zero-based position of this message in the request array.
success:
type: boolean
data:
type: object
properties:
id:
type: string
description: Persisted sent-email attempt ID.
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
from:
type: string
description: |
Bare from-address actually written on the wire. Echoed
on every success branch so callers can confirm what
went out, particularly useful for the /emails/{id}/reply
path where `from` is server-derived from the inbound's
recipient when the caller doesn't override.
For sends where the caller passed a from-header that
included a display name (e.g. `"Acme Support" `),
this field is the parsed bare address (`[email protected]`).
The display name was sent on the wire intact; this field
just makes the address easy to compare against allowlists.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's OUTBOUND relay
(the box that signs your mail and submits it to the
receiving MTA). NOT the receiver's queue id.
The receiver may also report its own queue id in
`smtp_response_text` (e.g. `"250 2.0.0 Ok: queued as
99D111927CDA"` from a Postfix receiver). Those two ids
refer to different mail systems and are NOT comparable.
Treat `queue_id` as Primitive-internal and the
receiver's id as remote-system-internal.
Null on rows that never reached the relay (queued,
gate_denied, agent_failed before signing).
accepted:
type: array
items:
type: string
description: Recipient addresses accepted by the relay.
rejected:
type: array
items:
type: string
description: Recipient addresses rejected by the relay.
client_idempotency_key:
type: string
description: Effective idempotency key used for this send.
request_id:
type: string
description: Server-issued request identifier for support and tracing.
content_hash:
type: string
description: Stable hash of the canonical send payload.
delivery_status:
type: string
description: |
Narrower enum covering only the four terminal delivery
outcomes returned to a synchronous `wait: true` send.
On the SendMailResult shape, `delivery_status` is always
equal to `status` whenever both are present (i.e. on
terminal-state replays and live wait=true responses).
The two fields exist so callers that want to type-narrow
on "this is a delivery outcome" can pattern-match against
the four-value enum without handling the broader
SentEmailStatus value set (which also covers `queued`,
`submitted_to_agent`, `agent_failed`, `gate_denied`,
`unknown`).
On async-mode and pre-terminal responses, `delivery_status`
is absent and only `status` is populated. Use `status` if
you want a single field that's always present.
enum:
- delivered
- bounced
- deferred
- wait_timeout
smtp_response_code:
type:
- integer
- "null"
description: SMTP response code from the first downstream delivery outcome when wait is true.
smtp_response_text:
type: string
description: SMTP response text from the first downstream delivery outcome when wait is true.
idempotent_replay:
type: boolean
description: |
True when the response replays a previously-recorded send
keyed by `client_idempotency_key` (same key, same canonical
payload). False on a fresh send and on gate-denied
responses. Lets callers branch on cache state without
diffing fields.
required:
- id
- status
- from
- queue_id
- accepted
- rejected
- client_idempotency_key
- request_id
- content_hash
- idempotent_replay
error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
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
"401":
$ref: "#/components/responses/Unauthorized"
description: Invalid or missing API key
"429":
$ref: "#/components/responses/RateLimited"
description: Rate limit exceeded
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
/demo/jobs:
post:
operationId: startDemoJob
summary: Start a sample asynchronous job without authentication
description: Public, no-auth demo of the asynchronous-job pattern. Starts a synthetic job and returns `202 Accepted` with a `Location` header (and `data.status_url`) pointing at the job-status resource. Poll `GET /demo/jobs/{id}` until the status is terminal. No real work runs; the status is a fixed sample.
tags:
- Demo
security: []
responses:
"202":
description: Job accepted. Poll the returned status URL for progress.
headers:
Location:
schema:
type: string
format: uri
description: URL of the job-status resource to poll.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
demo:
type: boolean
const: true
note:
type: string
data:
type: object
properties:
id:
type: string
type:
type: string
status:
type: string
enum:
- queued
- processing
- completed
- failed
status_url:
type: string
format: uri
created_at:
type: string
format: date-time
required:
- id
- status
- status_url
required:
- success
- demo
- data
"429":
$ref: "#/components/responses/RateLimited"
description: Demo rate limit reached.
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
/demo/jobs/{id}:
get:
operationId: getDemoJob
summary: Poll a sample asynchronous job without authentication
description: Public, no-auth demo. Returns the status of a synthetic job started by `POST /demo/jobs` â the poll half of the async-job pattern. No real work runs; the status is a fixed sample.
tags:
- Demo
security: []
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Job id returned by POST /demo/jobs.
responses:
"200":
description: Current job status.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
demo:
type: boolean
const: true
note:
type: string
data:
type: object
properties:
id:
type: string
type:
type: string
status:
type: string
enum:
- queued
- processing
- completed
- failed
progress:
type: integer
created_at:
type: string
format: date-time
estimated_completion_at:
type: string
format: date-time
required:
- id
- status
- progress
required:
- success
- demo
- data
"429":
$ref: "#/components/responses/RateLimited"
description: Demo rate limit reached.
/demo/emails:
get:
operationId: getDemoEmails
summary: List sample emails without authentication
description: Public, no-auth demo. Returns synthetic email objects shaped exactly like the authenticated `GET /emails` response (same `EmailSummary` schema), so an arriving agent can read the data model before obtaining credentials. No real tenant data is exposed; every object is a fixed sample. Rate-limited per IP.
tags:
- Demo
security: []
responses:
"200":
description: Synthetic list of emails, same shape as the authenticated /emails response.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
demo:
type: boolean
const: true
note:
type: string
description: Reminder that the data is synthetic.
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
status:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. For most legitimate mail this equals the bare
address in the From header; for mailing lists, bounce
handlers, and forwarders it is typically the bounce address
rather than the human-visible sender.
For the parsed From-header value (with display name handling
and a sender-fallback when the header is unparseable), GET
the email by id and use `from_email`.
recipient:
type: string
subject:
type:
- string
- "null"
domain:
type: string
spam_score:
type:
- number
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
raw_size_bytes:
type:
- integer
- "null"
webhook_status:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
webhook_attempt_count:
type: integer
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Fetch
`/threads/{thread_id}` for the full ordered thread. NULL on
messages received before threading was enabled.
required:
- id
- status
- sender
- recipient
- domain
- created_at
- received_at
- webhook_attempt_count
required:
- success
- demo
- data
"429":
$ref: "#/components/responses/RateLimited"
description: Demo rate limit reached.
/demo/emails/{id}:
get:
operationId: getDemoEmail
summary: Get one sample email without authentication
description: Public, no-auth demo. Returns a single synthetic email shaped exactly like the authenticated `GET /emails/{id}` response (`EmailDetail` schema). No real tenant data is exposed. Rate-limited per IP.
tags:
- Demo
security: []
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Any value; the demo returns a fixed sample.
responses:
"200":
description: Synthetic email detail, same shape as the authenticated /emails/{id} response.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
const: true
demo:
type: boolean
const: true
note:
type: string
description: Reminder that the data is synthetic.
data:
type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. Same value as `smtp_mail_from`; both fields exist
so protocol-aware tooling can use whichever name it expects.
For most legitimate mail this equals `from_email`; for
mailing lists, bounce handlers, and forwarders it is
typically the bounce-handling address rather than the
human-visible sender.
**For the canonical "who sent this email" value, use
`from_email`.**
recipient:
type: string
subject:
type:
- string
- "null"
body_text:
type:
- string
- "null"
description: Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed.
body_html:
type:
- string
- "null"
description: HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed.
status:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
domain:
type: string
spam_score:
type:
- number
- "null"
raw_size_bytes:
type:
- integer
- "null"
raw_sha256:
type:
- string
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
rejection_reason:
type:
- string
- "null"
webhook_status:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
webhook_attempt_count:
type: integer
webhook_last_attempt_at:
type:
- string
- "null"
format: date-time
webhook_last_status_code:
type:
- integer
- "null"
webhook_last_error:
type:
- string
- "null"
webhook_fired_at:
type:
- string
- "null"
format: date-time
smtp_helo:
type:
- string
- "null"
smtp_mail_from:
type:
- string
- "null"
description: |
SMTP envelope MAIL FROM (return-path), as accepted by the
inbound mail server. Same value as `sender`; both fields
exist so protocol-aware tooling can use whichever name it
expects.
For the canonical "who sent this email" value (display name
stripped, From-header preferred), use `from_email`.
smtp_rcpt_to:
type:
- array
- "null"
items:
type: string
from_header:
type:
- string
- "null"
description: |
Raw `From:` header from the message body, including any
display name (e.g. `"Alice Example" `).
Use this when you need the display name for rendering.
For the bare email address (display name stripped), use
`from_email`.
content_discarded_at:
type:
- string
- "null"
format: date-time
content_discarded_by_delivery_id:
type:
- string
- "null"
from_email:
type: string
description: |
Bare email address parsed from the `From:` header, with
display name stripped (e.g. `[email protected]`). Falls
back to `sender` (the SMTP envelope MAIL FROM) when the
`From:` header cannot be parsed.
**This is the canonical "who sent this email" field for
most use cases**, including comparing against allowlists,
routing replies, or displaying the sender to a user. Use
`from_header` when you specifically need the display name,
or `sender`/`smtp_mail_from` when you need the SMTP
envelope value (e.g. to follow a bounce).
to_email:
type: string
description: Parsed to address (same as recipient)
from_known_address:
type: boolean
description: |
True when the inbound's sender address has a matching grant
in the org's known-send-addresses list. Advisory: a true
value does not by itself guarantee that a reply will be
accepted by send-mail's gates; the per-send check at send
time remains authoritative.
replies:
type: array
description: |
Sent emails recorded as replies to this inbound, in send
order (ascending). Populated when a customer's send-mail
request carries an `in_reply_to` Message-ID that matches
this inbound's `message_id` in the same org. Includes
attempts that were gate-denied, so the array reflects every
recorded reply attempt regardless of outcome.
items:
type: object
properties:
id:
type: string
format: uuid
description: Sent-email row id.
status:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
to_address:
type: string
description: Recipient address as recorded on the sent_emails row.
subject:
type:
- string
- "null"
created_at:
type: string
format: date-time
queue_id:
type:
- string
- "null"
description: Outbound relay queue identifier when available.
required:
- id
- status
- to_address
- created_at
reply_to_sent_email_id:
type:
- string
- "null"
format: uuid
description: |
The `sent_emails.id` of the outbound this inbound was a
reply to, when resolvable. Set at inbound ingest by
matching the parsed In-Reply-To (or References, as a
fallback) against `sent_emails.message_id` in the same
org. The mirror of `sent_emails.in_reply_to_email_id` for
the inbound side of a thread. NULL when the inbound is
not a threaded reply to one of your sends, when neither
header survived the path through intermediate MTAs, or on
inbound received before this auto-link landed.
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Inbound and
outbound messages in the same conversation share a
`thread_id`; fetch `/threads/{thread_id}` for the full
ordered thread. Assigned at ingest. NULL on messages
received before threading was enabled (until backfilled).
parsed:
allOf:
- type: object
description: |
Parsed MIME content for an inbound email. Mirrors the
`email.parsed` object on the webhook payload so a single parser
handles both surfaces. `status` is `complete` when parsing
succeeded; on `failed` the body/address/attachment fields are
absent and `error` describes why.
properties:
status:
type: string
enum:
- complete
- failed
body_text:
type:
- string
- "null"
description: Plain-text body. Present when `status` is `complete`.
body_html:
type:
- string
- "null"
description: HTML body. Present when `status` is `complete`.
reply_to:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `Reply-To` header addresses.
cc:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `Cc` header addresses.
bcc:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `Bcc` header addresses (rarely present on inbound).
to_addresses:
type:
- array
- "null"
items:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
description: Parsed `To` header addresses.
in_reply_to:
type:
- array
- "null"
items:
type: string
description: Message-IDs from the `In-Reply-To` header.
references:
type:
- array
- "null"
items:
type: string
description: Message-IDs from the `References` header.
attachments:
type: array
items:
type: object
description: |
Metadata for one attachment. The bytes are not inline; download
all attachments for a message as a gzipped tarball via
`/emails/{id}/attachments.tar.gz`. `sha256` lets you verify a
specific part after extraction.
properties:
filename:
type:
- string
- "null"
content_type:
type:
- string
- "null"
size_bytes:
type: integer
sha256:
type:
- string
- "null"
part_index:
type: integer
description: Zero-based index of this part within the message.
required:
- size_bytes
description: Attachment metadata. Empty array when none.
error:
type:
- object
- "null"
description: |
Present (non-null) only when `status` is `failed`. When
present, all three fields are populated, so a consumer can
branch on `code` without defensive null checks.
properties:
code:
type: string
description: Stable failure code (e.g. `PARSE_FAILED`).
message:
type: string
retryable:
type: boolean
required:
- code
- message
- retryable
required:
- status
description: |
Parsed MIME content (addresses, threading headers,
attachment metadata), matching the `email.parsed` object
on the webhook payload so one parser handles both the
webhook and this endpoint. The top-level `body_text` /
`body_html` fields above are the same values as
`parsed.body_text` / `parsed.body_html`, retained for
backward compatibility.
auth:
allOf:
- type: object
description: |
SPF / DKIM / DMARC verdicts computed at ingest. Mirrors the
`email.auth` object on the webhook payload. Field names are
camelCase to match that payload exactly. For messages received
before auth was recorded, the verdicts default to `none`.
properties:
spf:
type: string
description: SPF result (e.g. `pass`, `fail`, `softfail`, `none`).
dmarc:
type: string
description: DMARC result (e.g. `pass`, `fail`, `none`).
dmarcPolicy:
type:
- string
- "null"
description: Published DMARC policy (`none`, `quarantine`, `reject`).
dmarcFromDomain:
type:
- string
- "null"
description: The From-header domain DMARC was evaluated against.
dmarcSpfAligned:
type: boolean
dmarcDkimAligned:
type: boolean
dmarcSpfStrict:
type:
- boolean
- "null"
dmarcDkimStrict:
type:
- boolean
- "null"
dkimSignatures:
type: array
items:
type: object
description: One DKIM signature found on the message, with its verdict.
properties:
domain:
type: string
selector:
type: string
result:
type: string
description: Verification result (e.g. `pass`, `fail`, `none`).
aligned:
type: boolean
description: Whether the signing domain aligns with the From domain (for DMARC).
keyBits:
type:
- integer
- "null"
algo:
type:
- string
- "null"
required:
- domain
- selector
- result
- aligned
required:
- spf
- dmarc
- dmarcSpfAligned
- dmarcDkimAligned
- dkimSignatures
description: |
SPF / DKIM / DMARC verdicts computed at ingest, matching
the `email.auth` object on the webhook payload. Use these
to decide how much to trust a message before acting on
instructions it contains.
required:
- id
- sender
- recipient
- status
- domain
- created_at
- received_at
- webhook_attempt_count
- from_email
- to_email
- replies
- parsed
- auth
required:
- success
- demo
- data
"429":
$ref: "#/components/responses/RateLimited"
description: Demo rate limit reached.
/discovery:
get:
operationId: getDiscovery
summary: List the unauthenticated API endpoints
description: "Public, no-auth entry point for arriving agents. Returns the API base URL, the authentication lifecycle (register/claim/revoke), and the list of operations callable without credentials â the agent/CLI signup + login flows and the no-account `POST /send-mail/demo`. Fetch this first to discover how to obtain credentials, then call the authenticated operations with `Authorization: Bearer prim_`."
tags:
- Discovery
security: []
responses:
"200":
description: Discovery document listing the unauthenticated endpoints and how to obtain credentials.
content:
application/json:
schema:
type: object
properties:
service:
type: string
description:
type: string
base_url:
type: string
format: uri
documentation:
type: string
format: uri
openapi:
type: string
format: uri
authentication:
type: object
properties:
guide:
type: string
format: uri
register_uri:
type: string
format: uri
claim_uri:
type: string
format: uri
revocation_uri:
type: string
format: uri
required:
- guide
- register_uri
- claim_uri
- revocation_uri
public_endpoints:
type: array
items:
type: object
properties:
method:
type: string
path:
type: string
url:
type: string
format: uri
summary:
type: string
authentication:
type: string
const: none
required:
- method
- path
- url
- summary
- authentication
required:
- service
- base_url
- authentication
- public_endpoints
example:
service: Primitive â email infrastructure for AI agents
description: "These endpoints are callable without authentication. Use the signup flows to register an agent identity and obtain credentials, or the demo to exercise the API before you have one. Every other endpoint requires `Authorization: Bearer `."
base_url: https://api.primitive.dev/v1
documentation: https://docs.primitive.dev/docs
openapi: https://www.primitive.dev/openapi.json
authentication:
guide: https://www.primitive.dev/auth.md
register_uri: https://api.primitive.dev/v1/agent/signup/start
claim_uri: https://api.primitive.dev/v1/agent/signup/verify
revocation_uri: https://www.primitive.dev/oauth/revoke
public_endpoints:
- method: POST
path: /agent/signup/start
url: https://api.primitive.dev/v1/agent/signup/start
summary: Start agent account signup
authentication: none
- method: POST
path: /send-mail/demo
url: https://api.primitive.dev/v1/send-mail/demo
summary: Try send-mail without authentication (simulation â no mail is sent)
authentication: none
/ask:
servers:
- url: https://www.primitive.dev
description: Web origin â NLWeb endpoint host (not the v1 API base).
get:
operationId: askGet
summary: Ask about Primitive â NLWeb query (no authentication)
description: "Microsoft NLWeb natural-language query endpoint. Returns structured JSON describing Primitive. **No credentials required.** Pass the question as `?q=...`; send `Accept: text/event-stream` for an SSE stream."
tags:
- Discovery
security: []
parameters:
- name: q
in: query
required: false
schema:
type: string
maxLength: 500
description: Natural-language question about Primitive.
responses:
"200":
description: NLWeb result list describing Primitive capabilities.
content:
application/json:
schema:
type: object
properties:
_meta:
type: object
properties:
response_type:
type: string
version:
type: string
query:
type: string
results:
type: array
items:
type: object
properties:
"@context":
type: string
"@type":
type: string
name:
type: string
url:
type: string
description:
type: string
required:
- _meta
- results
post:
operationId: ask
summary: Ask about Primitive â NLWeb query (no authentication)
description: 'Microsoft NLWeb natural-language query endpoint. POST `{ "q": "..." }`. **No credentials required.** Set `prefer.streaming: true` or `Accept: text/event-stream` for an SSE stream.'
tags:
- Discovery
security: []
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
q:
type: string
maxLength: 500
description: Natural-language question about Primitive.
prefer:
type: object
properties:
streaming:
type: boolean
example:
q: What does Primitive cost?
responses:
"200":
description: NLWeb result list describing Primitive capabilities.
content:
application/json:
schema:
type: object
properties:
_meta:
type: object
properties:
response_type:
type: string
version:
type: string
query:
type: string
results:
type: array
items:
type: object
properties:
"@context":
type: string
"@type":
type: string
name:
type: string
url:
type: string
description:
type: string
required:
- _meta
- results
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
/health:
get:
operationId: getHealth
summary: Service health check
description: Liveness probe for the API. Returns 200 with a small status payload whenever the service is up. Public â no authentication required â so agents, uptime monitors, and load balancers can reach it without a credential.
tags:
- Service
security: []
responses:
"200":
description: The service is healthy.
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
status:
type: string
enum:
- ok
description: Always `ok` when the service is healthy.
env:
type: string
description: Deployment environment identifier.
build_sha:
type: string
description: Git commit SHA of the running build.
ts:
type: string
format: date-time
description: Server timestamp (ISO 8601).
required:
- status
- env
- build_sha
- ts
example:
status: ok
env: production
build_sha: 0c0f184
ts: 2026-06-10T10:00:00.000Z
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: "API key with `prim_` prefix or OAuth access token with `prim_oat_` prefix: `Authorization: Bearer `. Access is governed by the caller's organization role (`owner`, `admin`, or `member`): API keys always act at `member` level regardless of who created them, and OAuth access tokens act with the authorizing user's current organization role, resolved per request. Every operation in this spec is available to organization members; billing and organization administration are owner/admin actions performed in the dashboard and are not part of this API."
DownloadToken:
type: apiKey
in: query
name: token
description: Signed download token provided in webhook payloads
parameters:
ResourceId:
name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
IdempotencyKey:
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
Cursor:
name: cursor
in: query
schema:
type: string
description: |
Pagination cursor from a previous response's `meta.cursor` field.
Format: `{ISO-datetime}|{id}`
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
description: Number of results per page
MemoryKeyQuery:
name: key
in: query
required: true
description: Memory key. Must be at most 512 UTF-8 bytes.
schema:
type: string
minLength: 1
maxLength: 512
MemoryScopeQueryType:
name: scope_type
in: query
required: false
description: |
Explicit scope type. Omit to use automatic scope resolution. Pass
`function` with `scope_id=`, or `org` with no `scope_id`.
schema:
type: string
enum:
- org
- function
MemoryScopeId:
name: scope_id
in: query
required: false
description: |
Function id UUID when `scope_type=function`. Not valid with
`scope_type=org`.
schema:
type: string
format: uuid
responses:
Unauthorized:
description: Invalid or missing API key
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: unauthorized
message: Invalid or missing API key
Forbidden:
description: Authenticated caller lacks permission for the operation
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: forbidden
message: Insufficient permissions
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: not_found
message: Resource not found
ValidationError:
description: Invalid request parameters
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: validation_error
message: Invalid domain format
PaymentRequired:
description: Usage credits are exhausted or payment is required.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
DeployFailed:
description: Function deploy could not be completed; previously deployed code remains live
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: deploy_failed
message: Function deploy failed
RateLimited:
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: rate_limit_exceeded
message: Rate limit exceeded
BadGateway:
description: Primitive could not complete the downstream SMTP request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: outbound_unreachable
message: Outbound SMTP service request failed
ServiceUnavailable:
description: Primitive is temporarily unable to process the request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: outbound_capacity_exhausted
message: Outbound capacity is temporarily exhausted
InternalError:
description: Primitive encountered an internal error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: internal_error
message: Internal server error
Conflict:
description: The request conflicts with the current state of the resource
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: conflict
message: settlement already in progress
UnprocessableEntity:
description: |
The request was well-formed but could not be processed. For Payments
this covers a missing payout address, a failed payment verification, a
spend-policy decline, or an expired challenge; `error.code` distinguishes
them.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: payment_declined
message: payment exceeds the per-payment cap
Deleted:
description: Resource deleted
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/SuccessEnvelope"
- type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
const: true
required:
- deleted
Gone:
description: Resource is no longer available.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: not_found
message: Resource is no longer available
PayloadTooLarge:
description: Request body is too large.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: validation_error
message: Request body is too large
UnsupportedMediaType:
description: Content-Type must be application/json.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: validation_error
message: Content-Type must be application/json
FailedDependency:
description: The operation depends on another service or step that failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: deploy_failed
message: Function deploy failed
GatewayTimeout:
description: Primitive timed out while waiting for a downstream operation.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
success: false
error:
code: internal_error
message: Downstream operation timed out
schemas:
PublishPolicy:
type: string
enum:
- owner_only
- request
- open
description: |
Who may publish into a registry. owner_only: only the registry owner.
request: anyone may request and the owner approves. open: anyone may
publish and it lists immediately (no approval step).
Registry:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
name:
type: string
description:
type:
- string
- "null"
is_public:
type: boolean
publish_policy:
$ref: "#/components/schemas/PublishPolicy"
required:
- id
- slug
- name
- description
- is_public
- publish_policy
RegistryAgent:
type: object
description: An agent's public directory profile.
properties:
address:
type: string
display_name:
type: string
title:
type:
- string
- "null"
description:
type:
- string
- "null"
tags:
type: array
items:
type: string
handle:
type:
- string
- "null"
description: The registry-scoped name. Null on the global by-address read.
last_reachable_at:
type:
- string
- "null"
format: date-time
description: When the agent's address was last confirmed to still route to its endpoint. A freshness signal for ranking listings; null until the first check.
required:
- address
- display_name
- title
- description
- tags
- handle
- last_reachable_at
RegistryRequest:
type: object
description: A pending publication request, as the registry owner sees it.
properties:
id:
type: string
format: uuid
address:
type: string
display_name:
type: string
handle:
type:
- string
- "null"
requested_at:
type: string
format: date-time
required:
- id
- address
- display_name
- handle
- requested_at
CreateRegistryInput:
type: object
properties:
slug:
type: string
pattern: ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$
description: Lowercase slug, unique across registries.
name:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
is_public:
type: boolean
publish_policy:
$ref: "#/components/schemas/PublishPolicy"
required:
- slug
- name
UpdateRegistryInput:
type: object
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
is_public:
type: boolean
publish_policy:
$ref: "#/components/schemas/PublishPolicy"
DefineAgentInput:
type: object
properties:
address:
type: string
description: The agent's globally unique email address; mail to it must route to an endpoint the account controls.
endpoint_id:
type: string
format: uuid
description: Optional. The endpoint the agent runs on. Omit it to resolve the endpoint from the address's routing automatically; supply it to pin a specific endpoint, which is then validated against the address's route.
display_name:
type: string
minLength: 1
maxLength: 120
title:
type:
- string
- "null"
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
tags:
type: array
items:
type: string
maxLength: 40
maxItems: 20
required:
- address
- display_name
PublishAgentInput:
type: object
description: Publish an agent into a registry. When display_name is present the agent identity is defined (create-or-get by address) in the same call before publishing; omit the define fields to publish an already-defined agent.
properties:
address:
type: string
handle:
type: string
description: The registry-scoped name to list the agent under.
display_name:
type: string
minLength: 1
maxLength: 120
description: Present to define the agent identity before publishing (define + publish in one call).
endpoint_id:
type: string
format: uuid
description: Optional, only used when defining. Omit to resolve the endpoint from the address's routing.
title:
type:
- string
- "null"
maxLength: 120
description:
type:
- string
- "null"
maxLength: 2000
tags:
type: array
items:
type: string
maxLength: 40
maxItems: 20
required:
- address
- handle
dependentRequired:
endpoint_id:
- display_name
title:
- display_name
description:
- display_name
tags:
- display_name
PublishAgentResult:
type: object
properties:
status:
type: string
enum:
- approved
- requested
description: approved lists immediately; requested pends owner approval.
handle:
type: string
idempotent_replay:
type: boolean
description: True when the publish matched an existing identical publication.
required:
- status
- handle
- idempotent_replay
DecideRegistryRequestInput:
type: object
properties:
decision:
type: string
enum:
- approved
- rejected
required:
- decision
RegisterPayoutAddressInput:
type: object
additionalProperties: false
properties:
address:
type: string
pattern: ^0x[0-9a-fA-F]{40}$
description: The payout address (your signer's own EVM address), 0x-prefixed.
network:
type: string
enum:
- base
- base-sepolia
description: The chain the address receives on.
signature:
type: string
pattern: ^0x[0-9a-fA-F]+$
description: |
A `personal_sign` signature over the org-bound message produced by
the SDK helper `buildPayoutRegistrationMessage`. Recovered and
checked against `address`; the org id is bound into the signed bytes.
issued_at:
type: string
format: date-time
description: |
ISO-8601 timestamp embedded in the signed message. Must be within a
short freshness window (about 10 minutes) of server time.
label:
type: string
maxLength: 80
description: Optional human-readable label.
required:
- address
- network
- signature
- issued_at
X402PayoutAddress:
type: object
properties:
id:
type: string
format: uuid
address:
type: string
description: The checksummed payout address.
network:
type: string
enum:
- base
- base-sepolia
label:
type:
- string
- "null"
is_default:
type: boolean
description: Exactly one address per (org, network) is the default.
verified_at:
type: string
format: date-time
description: When ownership of the address was last proven.
created_at:
type: string
format: date-time
required:
- id
- address
- network
- label
- is_default
- verified_at
CreateChallengeInput:
type: object
additionalProperties: false
properties:
amount:
type: string
pattern: ^[1-9][0-9]{0,38}$
description: |
Amount to collect, in token base units. USDC has 6 decimals, so
`"10000"` is 0.01 USDC.
network:
type: string
enum:
- base
- base-sepolia
payer_org:
type: string
format: uuid
description: |
The org id allowed to pay this challenge (on-net binding). Optional.
expires_in:
type: integer
minimum: 60
maximum: 86400
description: Seconds until the challenge expires. Defaults to 3600.
resource:
type: string
format: uri
maxLength: 2048
description: |
Optional URL identifying what is being paid for. Defaults to a
synthetic `x402:challenge:` identifier.
description:
type: string
maxLength: 512
description: Optional human-readable description of the payment.
required:
- amount
- network
X402NonceBinding:
type: object
description: |
The interaction binding the payer hashes into the EIP-3009 nonce
(`deriveEip3009Nonce`). Pinning the nonce to this binding is what lets an
x402 payment ride asynchronous transports safely: a replayed challenge
can't redirect funds and a signed payment can't settle twice.
properties:
interaction_id:
type: string
description: Interaction id, including its `@domain` part.
challenge_step_id:
type: string
format: uuid
challenge_nonce:
type: string
pattern: ^[0-9a-f]{64}$
description: 32 random bytes as 64 lowercase hex chars.
required:
- interaction_id
- challenge_step_id
- challenge_nonce
X402PaymentRequirements:
type: object
description: |
The x402 `PaymentRequirements` the payer signs over. Field names are
x402's native camelCase, preserved byte-for-byte.
properties:
scheme:
type: string
description: The x402 settlement scheme. Always `exact` for v1.
example: exact
network:
type: string
enum:
- base
- base-sepolia
maxAmountRequired:
type: string
description: Amount in token base units.
payTo:
type: string
description: The payee's resolved payout address (checksummed).
asset:
type: string
description: The token contract address (checksummed). USDC.
resource:
type: string
description:
type: string
maxTimeoutSeconds:
type: integer
extra:
type: object
description: |
The token's load-bearing EIP-712 domain params. `name` differs by
chain (Base mainnet USDC is `USD Coin`, Base Sepolia is `USDC`); a
wrong value produces a signature the verifier rejects.
properties:
name:
type: string
version:
type: string
required:
- name
- version
required:
- scheme
- network
- maxAmountRequired
- payTo
- asset
- extra
X402Challenge:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- pending
- settling
- settled
- failed
- expired
network:
type: string
enum:
- base
- base-sepolia
asset:
type: string
description: Token contract address (checksummed).
amount:
type: string
description: Amount in token base units.
pay_to:
type: string
description: The payee's resolved payout address (checksummed).
payer_org:
type:
- string
- "null"
description: The org id bound as payer, if one was set at creation.
resource:
type:
- string
- "null"
description:
type:
- string
- "null"
nonce_binding:
$ref: "#/components/schemas/X402NonceBinding"
settle_tx:
type:
- string
- "null"
description: On-chain settlement transaction hash once settled.
settled_at:
type:
- string
- "null"
format: date-time
failure_reason:
type:
- string
- "null"
expires_at:
type: string
format: date-time
created_at:
type: string
format: date-time
payment_requirements:
description: |
Present on the create response. Hand the whole challenge (including
this) to the payer; `getChallenge` omits it (it is for status polling
by the challenger).
allOf:
- $ref: "#/components/schemas/X402PaymentRequirements"
required:
- id
- status
- network
- asset
- amount
- pay_to
- nonce_binding
- expires_at
CreateEmailChallengeInput:
type: object
additionalProperties: false
description: |
Issue a payment challenge over an email thread. `from` is your sending
address (the funds receiver; ownership is enforced at send, exactly as
for outbound mail) and `to` is the payer's address. The `pay_to` payout
wallet and the token asset are resolved server-side, never taken from
the request.
properties:
from:
type: string
format: email
description: |
Your sending address (the payee / funds receiver). Must be an
address your org is allowed to send from.
to:
type: string
format: email
description: The payer's email address the challenge is sent to.
amount:
type: string
pattern: ^[1-9][0-9]{0,38}$
description: |
Amount to collect, in token base units (unlike the `charge` CLI
command, which also accepts `--amount-usdc`, this field takes base
units only). USDC has 6 decimals, so `"10000"` is 0.01 USDC:
multiply a human USDC amount by 1,000,000 (0.01 USDC -> `"10000"`).
network:
type: string
enum:
- base
- base-sepolia
expires_in:
type: integer
minimum: 60
maximum: 86400
description: Seconds until the challenge expires. Defaults to 300.
resource:
type: string
format: uri
maxLength: 2048
description: Optional URL identifying what is being paid for.
description:
type: string
maxLength: 512
description: Optional human-readable description of the payment.
required:
- from
- to
- amount
- network
X402EmailChallengeDetails:
type: object
description: |
The challenge the payer needs to sign and pay, carried inside an
email-native challenge response.
properties:
payment_requirements:
$ref: "#/components/schemas/X402PaymentRequirements"
nonce_binding:
$ref: "#/components/schemas/X402NonceBinding"
expires_at:
type: string
format: date-time
description: ISO-8601 expiry of the challenge.
required:
- payment_requirements
- nonce_binding
- expires_at
X402EmailChallenge:
type: object
description: |
The result of issuing an email-native payment challenge. `interaction_id`
is the real email thread id (`uuid@domain`) the payment is bound to;
`challenge_id` is the underlying challenge record. Hand the `challenge`
to the payer, who replies with a signed `payment` interaction step (the
SDK `payEmailChallenge` helper builds it).
properties:
interaction_id:
type: string
description: The email thread id (`uuid@domain`) the payment is bound to.
challenge_id:
type: string
format: uuid
description: The underlying challenge record id.
challenge:
$ref: "#/components/schemas/X402EmailChallengeDetails"
required:
- interaction_id
- challenge_id
- challenge
X402PaymentPayload:
type: object
description: |
A signed x402 v1 `PaymentPayload`. The SDK `pay()` helper builds this;
callers rarely construct it by hand. Field names are x402-native.
properties:
x402Version:
type: integer
const: 1
scheme:
type: string
const: exact
network:
type: string
enum:
- base
- base-sepolia
payload:
type: object
properties:
signature:
type: string
pattern: ^0x[0-9a-fA-F]+$
description: The EIP-712 signature over the authorization.
authorization:
type: object
description: The EIP-3009 `transferWithAuthorization` fields, as strings.
properties:
from:
type: string
to:
type: string
value:
type: string
validAfter:
type: string
validBefore:
type: string
nonce:
type: string
required:
- from
- to
- value
- validAfter
- validBefore
- nonce
required:
- signature
- authorization
required:
- x402Version
- scheme
- network
- payload
PayChallengeInput:
type: object
additionalProperties: false
properties:
payment:
$ref: "#/components/schemas/X402PaymentPayload"
required:
- payment
X402Receipt:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- settled
settle_tx:
type:
- string
- "null"
description: On-chain settlement transaction hash.
required:
- id
- status
- settle_tx
X402SpendPolicy:
type: object
description: |
The payer's outbound spend policy. Returned with defaults (not paused,
no caps, any on-net payee) when none is set.
properties:
paused:
type: boolean
description: Kill-switch. When true, all outbound payments are refused.
max_per_payment:
type:
- string
- "null"
description: Per-payment cap in token base units, or null for no cap.
max_per_day:
type:
- string
- "null"
description: Rolling-day cap in token base units, or null for no cap.
allowlist:
type:
- array
- "null"
items:
type: string
format: uuid
description: |
Allowed payee org ids. `null` allows any on-net payee; `[]` denies
all.
required:
- paused
- max_per_payment
- max_per_day
- allowlist
UpdateSpendPolicyInput:
type: object
additionalProperties: false
description: |
Merge update: only the fields you include change; omit a field to keep
its current value; send `null` to clear a cap.
properties:
paused:
type: boolean
max_per_payment:
type:
- string
- "null"
pattern: ^[1-9][0-9]{0,38}$
max_per_day:
type:
- string
- "null"
pattern: ^[1-9][0-9]{0,38}$
allowlist:
type:
- array
- "null"
maxItems: 1000
items:
type: string
format: uuid
X402DeclinedPayment:
type: object
description: A payment the org's spend policy refused.
properties:
id:
type: string
format: uuid
challenge_id:
type:
- string
- "null"
format: uuid
description: The challenge that was declined, if still present.
counterparty_org:
type:
- string
- "null"
format: uuid
description: The payee (challenger) org, when known.
network:
type: string
enum:
- base
- base-sepolia
amount:
type: string
description: Amount in token base units.
reason:
type: string
description: Why the payment was declined (cap, allowlist, paused).
declined_at:
type: string
format: date-time
required:
- id
- network
- amount
- reason
- declined_at
SuccessEnvelope:
type: object
properties:
success:
type: boolean
const: true
required:
- success
- data
ListEnvelope:
type: object
properties:
success:
type: boolean
const: true
meta:
$ref: "#/components/schemas/PaginationMeta"
required:
- success
- data
- meta
PaginationMeta:
type: object
properties:
total:
type: integer
description: Total number of matching records
limit:
type: integer
description: Page size used for this request
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if no more results
required:
- total
- limit
- cursor
ErrorResponse:
type: object
properties:
success:
type: boolean
const: false
error:
type: object
properties:
code:
type: string
enum:
- unauthorized
- forbidden
- not_found
- validation_error
- rate_limit_exceeded
- internal_error
- conflict
- mx_conflict
- outbound_disabled
- cannot_send_from_domain
- recipient_not_allowed
- outbound_key_missing
- outbound_unreachable
- outbound_key_invalid
- outbound_capacity_exhausted
- outbound_response_malformed
- outbound_relay_failed
- discard_not_enabled
- inbound_not_repliable
- search_timeout
- authorization_pending
- slow_down
- access_denied
- expired_token
- invalid_device_code
- invalid_signup_code
- invalid_signup_token
- invalid_verification_code
- email_delivery_failed
- clerk_signup_failed
- no_orgs_for_user
- org_not_accessible
- feature_disabled
- memory_conflict
- developer_usage_credit_exhausted
- no_payout_address
- ownership_proof_failed
- payment_verification_failed
- payment_declined
- challenge_expired
- settlement_failed
message:
type: string
details:
type: object
description: |
Optional structured data that callers can inspect to recover
from the error. The fields present depend on `code`. Additional
keys may be added over time without a major-version bump.
additionalProperties: true
properties:
mx_conflict:
type: object
description: Present when `code == mx_conflict`.
required:
- provider_name
- suggested_subdomain
properties:
provider_name:
type: string
description: Human-readable name of the detected mailbox provider (e.g. "Google Workspace").
suggested_subdomain:
type: string
description: Subdomain to try instead (e.g. "mail" for `mail.example.com`).
required_entitlements:
type: array
items:
type: string
description: Entitlements that would allow a denied send when no recipient-scope gate was granted.
sent_email_id:
type: string
description: ID of the persisted sent-email attempt associated with the error.
content_hash:
type: string
description: Content hash of the original request on idempotency cache-hit errors.
client_idempotency_key:
type: string
description: Effective idempotency key associated with the original request.
gates:
type: array
items:
$ref: "#/components/schemas/GateDenial"
description: Structured per-gate denial detail for recipient-scope send-mail failures.
request_id:
type: string
description: Server-issued request identifier for support and tracing.
required:
- code
- message
required:
- success
- error
GateDenial:
type: object
properties:
name:
type: string
enum:
- send_to_confirmed_domains
- send_to_known_addresses
description: Public recipient-scope gate name that denied the send.
reason:
type: string
enum:
- domain_not_confirmed
- recipient_unauthenticated
- recipient_not_known
description: Stable machine-readable denial reason.
message:
type: string
description: Human-readable explanation of the gate denial.
subject:
type: string
description: Domain or address the gate evaluated.
fix:
$ref: "#/components/schemas/GateFix"
docs_url:
type: string
description: Public docs URL with more context.
required:
- name
- reason
- message
- subject
GateFix:
type: object
properties:
action:
type: string
enum:
- confirm_domain
- sender_must_fix_authentication
- wait_for_inbound
description: Suggested next action for the caller.
subject:
type: string
description: Entity the action applies to.
required:
- action
- subject
StartCliLoginInput:
type: object
additionalProperties: false
properties:
device_name:
type: string
minLength: 1
maxLength: 80
description: Human-readable device name shown during browser approval
metadata:
type: object
additionalProperties: true
description: Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer
CliLoginStartResult:
type: object
properties:
device_code:
type: string
description: Opaque code used by the CLI to poll for approval
user_code:
type: string
pattern: ^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$
description: Short code the user confirms in the browser
verification_uri:
type: string
description: Browser URL where the user approves the login
verification_uri_complete:
type: string
description: Browser URL with the user code prefilled
expires_in:
type: integer
description: Seconds until the login session expires
interval:
type: integer
description: Minimum seconds between poll requests
required:
- device_code
- user_code
- verification_uri
- verification_uri_complete
- expires_in
- interval
PollCliLoginInput:
type: object
additionalProperties: false
properties:
device_code:
type: string
minLength: 1
required:
- device_code
CliLoginPollResult:
type: object
properties:
api_key:
type: string
description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
key_id:
type: string
format: uuid
description: Legacy alias for oauth_grant_id
key_prefix:
type: string
description: Legacy display prefix derived from access_token
access_token:
type: string
description: OAuth access token for CLI API authentication
refresh_token:
type: string
description: OAuth refresh token used by the CLI to renew access
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until access_token expires
auth_method:
type: string
enum:
- oauth
oauth_grant_id:
type: string
format: uuid
oauth_client_id:
type: string
org_id:
type: string
format: uuid
org_name:
type:
- string
- "null"
required:
- api_key
- key_id
- key_prefix
- access_token
- refresh_token
- token_type
- expires_in
- auth_method
- oauth_grant_id
- oauth_client_id
- org_id
- org_name
StartCliSignupInput:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
maxLength: 254
signup_code:
type: string
minLength: 1
maxLength: 128
description: Optional signup code. Omit if you do not have one.
terms_accepted:
type: boolean
const: true
description: Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy
device_name:
type: string
minLength: 1
maxLength: 80
description: Human-readable device name used for the created CLI OAuth grant
metadata:
type: object
additionalProperties: true
description: Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer
required:
- email
- terms_accepted
CliSignupStartResult:
type: object
properties:
signup_token:
type: string
description: Opaque token used to verify or resend the pending CLI signup
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- signup_token
- email
- expires_in
- resend_after
- verification_code_length
ResendCliSignupVerificationInput:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
required:
- signup_token
CliSignupResendResult:
type: object
properties:
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- email
- expires_in
- resend_after
- verification_code_length
VerifyCliSignupInput:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
verification_code:
type: string
minLength: 1
maxLength: 32
password:
type: string
minLength: 1
maxLength: 1024
required:
- signup_token
- verification_code
CliSignupVerifyResult:
type: object
properties:
api_key:
type: string
description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
key_id:
type: string
format: uuid
description: Legacy alias for oauth_grant_id
key_prefix:
type: string
description: Legacy display prefix derived from access_token
access_token:
type: string
description: OAuth access token for CLI API authentication
refresh_token:
type: string
description: OAuth refresh token used by the CLI to renew access
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until access_token expires
auth_method:
type: string
enum:
- oauth
oauth_grant_id:
type: string
format: uuid
oauth_client_id:
type: string
org_id:
type: string
format: uuid
org_name:
type:
- string
- "null"
required:
- api_key
- key_id
- key_prefix
- access_token
- refresh_token
- token_type
- expires_in
- auth_method
- oauth_grant_id
- oauth_client_id
- org_id
- org_name
StartAgentSignupInput:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
maxLength: 254
signup_code:
type: string
minLength: 1
maxLength: 128
description: Optional signup code. Omit if you do not have one.
terms_accepted:
type: boolean
const: true
description: Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy
device_name:
type: string
minLength: 1
maxLength: 80
description: Human-readable device name used for the created agent OAuth session
metadata:
type: object
additionalProperties: true
description: Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer
required:
- email
- terms_accepted
AgentSignupStartResult:
type: object
properties:
signup_token:
type: string
description: Opaque token used to verify or resend the pending agent signup
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- signup_token
- email
- expires_in
- resend_after
- verification_code_length
ResendAgentSignupVerificationInput:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
required:
- signup_token
AgentSignupResendResult:
type: object
properties:
email:
type: string
format: email
expires_in:
type: integer
description: Seconds until the pending signup expires
resend_after:
type: integer
description: Minimum seconds before requesting another verification email
verification_code_length:
type: integer
description: Number of digits in the emailed verification code
required:
- email
- expires_in
- resend_after
- verification_code_length
VerifyAgentSignupInput:
type: object
additionalProperties: false
properties:
signup_token:
type: string
minLength: 1
verification_code:
type: string
minLength: 1
maxLength: 32
org_id:
type: string
format: uuid
description: Optional workspace id to target when the verified email already belongs to multiple workspaces
required:
- signup_token
- verification_code
AgentOrgRef:
type: object
properties:
id:
type: string
format: uuid
name:
type:
- string
- "null"
required:
- id
- name
AgentSignupVerifyResult:
type: object
properties:
api_key:
type: string
description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
key_id:
type: string
format: uuid
description: Legacy alias for oauth_grant_id
key_prefix:
type: string
description: Legacy display prefix derived from access_token
access_token:
type: string
description: OAuth access token for CLI API authentication
refresh_token:
type: string
description: OAuth refresh token used by the CLI to renew access
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until access_token expires
auth_method:
type: string
enum:
- oauth
oauth_grant_id:
type: string
format: uuid
oauth_client_id:
type: string
org_id:
type: string
format: uuid
org_name:
type:
- string
- "null"
orgs:
type: array
items:
$ref: "#/components/schemas/AgentOrgRef"
description: Workspaces available to the verified email. The minted session targets `org_id`.
required:
- api_key
- key_id
- key_prefix
- access_token
- refresh_token
- token_type
- expires_in
- auth_method
- oauth_grant_id
- oauth_client_id
- org_id
- org_name
- orgs
PlanLimits:
type: object
description: Plan-derived quota limits for an account.
properties:
storage_mb:
type: number
send_per_hour:
type: number
send_per_day:
type: number
api_per_minute:
type: number
webhooks_max_global:
type:
- number
- "null"
webhooks_per_domain:
type: boolean
filters_per_domain:
type: boolean
spam_thresholds_per_domain:
type: boolean
required:
- storage_mb
- send_per_hour
- send_per_day
- api_per_minute
- webhooks_max_global
- webhooks_per_domain
- filters_per_domain
- spam_thresholds_per_domain
CreateAgentAccountInput:
type: object
additionalProperties: false
properties:
terms_accepted:
type: boolean
enum:
- true
description: Must be true to accept the Terms of Service and Privacy Policy.
device_name:
type: string
minLength: 1
maxLength: 80
description: Optional label for the device or agent creating the account.
required:
- terms_accepted
AgentAccountUpgradeHint:
type: object
description: In-band pointer to the upgrade path for an agent account.
properties:
plan:
type: string
enum:
- developer
description:
type: string
claim_path:
type: string
required:
- plan
- description
- claim_path
AgentAccountResult:
type: object
properties:
api_key:
type: string
description: One-time API key (prefixed `prim_`). Shown once; store it securely.
org_id:
type: string
format: uuid
address:
type:
- string
- "null"
description: Provisioned managed inbox FQDN, or null if the inbox publish was deferred.
plan:
type: string
enum:
- agent
limits:
$ref: "#/components/schemas/PlanLimits"
upgrade:
$ref: "#/components/schemas/AgentAccountUpgradeHint"
required:
- api_key
- org_id
- address
- plan
- limits
- upgrade
StartAgentClaimInput:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
maxLength: 254
description: Email to confirm. Must not already belong to a Primitive account.
required:
- email
AgentClaimStartResult:
type: object
properties:
claim_session_id:
type: string
resend_after_seconds:
type: integer
expires_in_seconds:
type: integer
required:
- claim_session_id
- resend_after_seconds
- expires_in_seconds
VerifyAgentClaimInput:
type: object
additionalProperties: false
properties:
verification_code:
type: string
minLength: 1
maxLength: 32
description: The verification code emailed by the claim start step.
required:
- verification_code
AgentClaimResult:
type: object
properties:
org_id:
type: string
format: uuid
plan:
type: string
enum:
- developer
email:
type: string
format: email
limits:
$ref: "#/components/schemas/PlanLimits"
required:
- org_id
- plan
- email
- limits
CreateAgentClaimLinkInput:
type: object
additionalProperties: false
description: No fields; an empty object is accepted.
properties: {}
AgentClaimLinkResult:
type: object
properties:
claim_token:
type: string
claim_url:
type:
- string
- "null"
description: Browser URL to hand to a human, or null if no web origin is configured.
expires_in_seconds:
type: integer
required:
- claim_token
- claim_url
- expires_in_seconds
CliLogoutInput:
type: object
additionalProperties: false
properties:
key_id:
type: string
format: uuid
description: Optional id guard; when provided it must match the authenticated OAuth grant id or API key id
CliLogoutResult:
type: object
properties:
revoked:
type: boolean
description: True when an OAuth grant was revoked. False for API-key-authenticated legacy logout, which only clears local CLI state.
key_id:
type: string
format: uuid
description: API key id for API-key-authenticated legacy logout
oauth_grant_id:
type: string
format: uuid
description: OAuth grant id revoked by OAuth-authenticated logout
required:
- revoked
Account:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
plan:
type: string
limits:
$ref: "#/components/schemas/PlanLimits"
entitlements:
type: array
items:
type: string
description: |
Granted org entitlement keys (sorted). A headless caller reads its
capabilities here â e.g. an emailless agent seeing only
["send_mail", "send_to_known_addresses"] knows it is reply-only.
managed_inbox_address:
type:
- string
- "null"
description: The managed inbox FQDN to reply as, or null if the org has no managed inbox.
created_at:
type: string
format: date-time
onboarding_completed:
type: boolean
onboarding_step:
type:
- string
- "null"
stripe_subscription_status:
type:
- string
- "null"
subscription_current_period_end:
type:
- string
- "null"
format: date-time
subscription_cancel_at_period_end:
type:
- boolean
- "null"
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
discard_content_on_webhook_confirmed:
type: boolean
webhook_secret_rotated_at:
type:
- string
- "null"
format: date-time
required:
- id
- email
- plan
- limits
- entitlements
- managed_inbox_address
- created_at
- discard_content_on_webhook_confirmed
AccountUpdated:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
plan:
type: string
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
discard_content_on_webhook_confirmed:
type: boolean
required:
- id
- email
- plan
- discard_content_on_webhook_confirmed
UpdateAccountInput:
type: object
additionalProperties: false
properties:
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
description: Global spam score threshold (0-15). Emails scoring above this are rejected. Set to null to disable.
discard_content_on_webhook_confirmed:
type: boolean
description: Whether to discard email content after the webhook endpoint confirms receipt.
minProperties: 1
StorageStats:
type: object
properties:
used_bytes:
type: integer
description: Total storage used in bytes
used_kb:
type: number
description: Total storage used in kilobytes (1 decimal)
used_mb:
type: number
description: Total storage used in megabytes (2 decimals)
quota_mb:
type: number
description: Storage quota in megabytes (based on plan)
percentage:
type: number
description: Percentage of quota used (1 decimal)
emails_count:
type: integer
description: Number of stored emails
required:
- used_bytes
- used_kb
- used_mb
- quota_mb
- percentage
- emails_count
WebhookSecret:
type: object
properties:
secret:
type: string
description: The webhook signing secret value
required:
- secret
InboxStatus:
type: object
additionalProperties: false
properties:
ready:
type: boolean
description: True when an active inbound domain and at least one processing route are both ready.
receiving_ready:
type: boolean
description: True when at least one active verified or managed domain can receive mail.
processing_ready:
type: boolean
description: True when at least one receiving-ready domain has an enabled webhook or function route.
summary:
type: string
next_actions:
type: array
items:
$ref: "#/components/schemas/InboxStatusNextAction"
domains:
type: array
items:
$ref: "#/components/schemas/InboxStatusDomain"
endpoints:
type: object
additionalProperties: false
properties:
total:
type: integer
enabled:
type: integer
disabled:
type: integer
fallback_enabled:
type: integer
domain_scoped_enabled:
type: integer
http_enabled:
type: integer
function_enabled:
type: integer
required:
- total
- enabled
- disabled
- fallback_enabled
- domain_scoped_enabled
- http_enabled
- function_enabled
functions:
type: object
additionalProperties: false
properties:
total:
type: integer
deployed:
type: integer
pending:
type: integer
failed:
type: integer
required:
- total
- deployed
- pending
- failed
recent_emails:
type: object
description: Inbound email activity from the last 30 days.
additionalProperties: false
properties:
total:
type: integer
description: Number of inbound emails received in the last 30 days.
latest_received_at:
type:
- string
- "null"
format: date-time
description: Most recent inbound email received in the last 30 days.
required:
- total
- latest_received_at
required:
- ready
- receiving_ready
- processing_ready
- summary
- next_actions
- domains
- endpoints
- functions
- recent_emails
InboxStatusNextAction:
type: object
additionalProperties: false
properties:
kind:
type: string
enum:
- add_domain
- verify_domain
- configure_processing
- send_test_email
- fix_failed_functions
message:
type: string
description: Human-readable next step.
command:
type: string
description: Suggested Primitive CLI command when there is an obvious next step.
required:
- kind
- message
InboxStatusDomain:
type: object
additionalProperties: false
properties:
id:
type: string
domain:
type: string
verified:
type: boolean
active:
type: boolean
managed:
type: boolean
receiving_ready:
type: boolean
processing_ready:
type: boolean
processing_route_count:
type: integer
endpoint_count:
type: integer
enabled_endpoint_count:
type: integer
function_endpoint_count:
type: integer
email_count:
type: integer
description: Number of inbound emails received for this domain in the last 30 days.
latest_email_received_at:
type:
- string
- "null"
format: date-time
description: Most recent inbound email received for this domain in the last 30 days.
status:
type: string
enum:
- ready
- stored_only
- pending_dns
- inactive
required:
- id
- domain
- verified
- active
- managed
- receiving_ready
- processing_ready
- processing_route_count
- endpoint_count
- enabled_endpoint_count
- function_endpoint_count
- email_count
- latest_email_received_at
- status
InboxStatusEndpointSummary:
type: object
additionalProperties: false
properties:
total:
type: integer
enabled:
type: integer
disabled:
type: integer
fallback_enabled:
type: integer
domain_scoped_enabled:
type: integer
http_enabled:
type: integer
function_enabled:
type: integer
required:
- total
- enabled
- disabled
- fallback_enabled
- domain_scoped_enabled
- http_enabled
- function_enabled
InboxStatusFunctionSummary:
type: object
additionalProperties: false
properties:
total:
type: integer
deployed:
type: integer
pending:
type: integer
failed:
type: integer
required:
- total
- deployed
- pending
- failed
InboxStatusRecentEmailSummary:
type: object
description: Inbound email activity from the last 30 days.
additionalProperties: false
properties:
total:
type: integer
description: Number of inbound emails received in the last 30 days.
latest_received_at:
type:
- string
- "null"
format: date-time
description: Most recent inbound email received in the last 30 days.
required:
- total
- latest_received_at
Domain:
description: |
A domain can be either verified or unverified. Verified domains have
`is_active` and `spam_threshold` fields. Unverified domains have a
`verification_token` and `dns_records` for DNS setup.
oneOf:
- $ref: "#/components/schemas/VerifiedDomain"
- $ref: "#/components/schemas/UnverifiedDomain"
VerifiedDomain:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain:
type: string
verified:
type: boolean
const: true
is_active:
type: boolean
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
verification_token:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- org_id
- domain
- verified
- is_active
- created_at
DomainDnsRecord:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- MX
- TXT
description: DNS record type.
name:
type: string
description: DNS-provider host/name value relative to the managed root zone.
fqdn:
type: string
description: Fully-qualified DNS record name.
value:
type: string
description: Exact value to publish.
priority:
type: integer
description: MX priority. Present only for MX records.
ttl:
type: integer
description: Suggested TTL in seconds when the API can provide one.
required:
type: boolean
const: true
purpose:
type: string
enum:
- inbound_mx
- ownership_verification
- spf
- dkim
- dmarc
- tls_reporting
status:
type: string
enum:
- pending
- found
- missing
- incorrect
message:
type: string
description: Short explanation of why this record is needed.
required:
- type
- name
- fqdn
- value
- required
- purpose
- status
UnverifiedDomain:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain:
type: string
verified:
type: boolean
const: false
verification_token:
type: string
description: Add this value as a TXT record to verify ownership
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
$ref: "#/components/schemas/DomainDnsRecord"
created_at:
type: string
format: date-time
required:
- id
- org_id
- domain
- verified
- verification_token
- created_at
AddDomainInput:
type: object
additionalProperties: false
properties:
domain:
type: string
minLength: 1
maxLength: 253
description: The domain name to claim (e.g. "example.com")
confirmed:
type: boolean
description: Set to true to confirm replacing an existing mailbox provider after an mx_conflict response.
outbound:
type: boolean
deprecated: true
description: Deprecated and ignored. Outbound DNS is provisioned for every new domain claim.
required:
- domain
UpdateDomainInput:
type: object
additionalProperties: false
properties:
is_active:
type: boolean
description: Whether the domain accepts incoming emails
spam_threshold:
type:
- number
- "null"
minimum: 0
maximum: 15
description: Per-domain spam threshold override (Pro plan required)
minProperties: 1
DomainVerifyResult:
oneOf:
- type: object
properties:
verified:
type: boolean
const: true
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
$ref: "#/components/schemas/DomainDnsRecord"
required:
- verified
- type: object
properties:
verified:
type: boolean
const: false
mxFound:
type: boolean
description: Whether MX records point to Primitive
txtFound:
type: boolean
description: Whether the TXT verification record was found
spfFound:
type: boolean
description: Whether the SPF record includes Primitive.
dkimFound:
type: boolean
description: Whether the DKIM public key record was found.
dmarcFound:
type: boolean
description: Whether the DMARC record was found.
tlsRptFound:
type: boolean
description: Whether the TLS-RPT record was found.
dns_records:
type: array
description: Exact DNS records to publish for a pending domain claim or verification attempt.
items:
$ref: "#/components/schemas/DomainDnsRecord"
error:
type: string
description: Human-readable verification failure reason
required:
- verified
- mxFound
- txtFound
- error
EmailSummary:
type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
status:
$ref: "#/components/schemas/EmailStatus"
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. For most legitimate mail this equals the bare
address in the From header; for mailing lists, bounce
handlers, and forwarders it is typically the bounce address
rather than the human-visible sender.
For the parsed From-header value (with display name handling
and a sender-fallback when the header is unparseable), GET
the email by id and use `from_email`.
recipient:
type: string
subject:
type:
- string
- "null"
domain:
type: string
spam_score:
type:
- number
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
raw_size_bytes:
type:
- integer
- "null"
webhook_status:
$ref: "#/components/schemas/EmailWebhookStatus"
webhook_attempt_count:
type: integer
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Fetch
`/threads/{thread_id}` for the full ordered thread. NULL on
messages received before threading was enabled.
required:
- id
- status
- sender
- recipient
- domain
- created_at
- received_at
- webhook_attempt_count
EmailSearchHighlights:
type: object
properties:
subject:
type: array
items:
type: string
description: Subject snippets with matching terms highlighted.
body:
type: array
items:
type: string
description: Body snippets with matching terms highlighted.
required:
- subject
- body
EmailSearchResult:
allOf:
- $ref: "#/components/schemas/EmailSummary"
- type: object
properties:
attachment_count:
type: integer
description: Number of parsed attachments on the email.
from_known_address:
type: boolean
description: Whether the parsed From address is known to this org from prior authenticated inbound mail.
score:
type: number
description: Relevance score. Present only when sorting by relevance.
highlights:
$ref: "#/components/schemas/EmailSearchHighlights"
required:
- attachment_count
- from_known_address
EmailSearchMeta:
type: object
properties:
total:
type: integer
description: Total number of matching records, capped when `total_capped` is true.
total_capped:
type: boolean
description: Whether `total` was capped instead of counted exactly.
limit:
type: integer
description: Page size used for this request.
cursor:
type:
- string
- "null"
description: Cursor for the next search page, or null if no more results.
sort:
type: string
enum:
- relevance
- received_at_desc
- received_at_asc
description: Sort mode used for the result page.
required:
- total
- total_capped
- limit
- cursor
- sort
EmailSearchFacetBucket:
type: object
properties:
value:
type:
- string
- "null"
count:
type: integer
required:
- value
- count
EmailSearchFacets:
type: object
properties:
by_sender:
type: array
items:
$ref: "#/components/schemas/EmailSearchFacetBucket"
by_domain:
type: array
items:
$ref: "#/components/schemas/EmailSearchFacetBucket"
by_status:
type: array
items:
$ref: "#/components/schemas/EmailSearchFacetBucket"
has_attachment:
type: object
properties:
"true":
type: integer
"false":
type: integer
required:
- "true"
- "false"
required:
- by_sender
- by_domain
- by_status
- has_attachment
EmailDetail:
type: object
properties:
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
org_id:
type:
- string
- "null"
format: uuid
sender:
type: string
description: |
SMTP envelope sender (return-path) the inbound mail server
accepted. Same value as `smtp_mail_from`; both fields exist
so protocol-aware tooling can use whichever name it expects.
For most legitimate mail this equals `from_email`; for
mailing lists, bounce handlers, and forwarders it is
typically the bounce-handling address rather than the
human-visible sender.
**For the canonical "who sent this email" value, use
`from_email`.**
recipient:
type: string
subject:
type:
- string
- "null"
body_text:
type:
- string
- "null"
description: Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed.
body_html:
type:
- string
- "null"
description: HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed.
status:
$ref: "#/components/schemas/EmailStatus"
domain:
type: string
spam_score:
type:
- number
- "null"
raw_size_bytes:
type:
- integer
- "null"
raw_sha256:
type:
- string
- "null"
created_at:
type: string
format: date-time
received_at:
type: string
format: date-time
rejection_reason:
type:
- string
- "null"
webhook_status:
$ref: "#/components/schemas/EmailWebhookStatus"
webhook_attempt_count:
type: integer
webhook_last_attempt_at:
type:
- string
- "null"
format: date-time
webhook_last_status_code:
type:
- integer
- "null"
webhook_last_error:
type:
- string
- "null"
webhook_fired_at:
type:
- string
- "null"
format: date-time
smtp_helo:
type:
- string
- "null"
smtp_mail_from:
type:
- string
- "null"
description: |
SMTP envelope MAIL FROM (return-path), as accepted by the
inbound mail server. Same value as `sender`; both fields
exist so protocol-aware tooling can use whichever name it
expects.
For the canonical "who sent this email" value (display name
stripped, From-header preferred), use `from_email`.
smtp_rcpt_to:
type:
- array
- "null"
items:
type: string
from_header:
type:
- string
- "null"
description: |
Raw `From:` header from the message body, including any
display name (e.g. `"Alice Example" `).
Use this when you need the display name for rendering.
For the bare email address (display name stripped), use
`from_email`.
content_discarded_at:
type:
- string
- "null"
format: date-time
content_discarded_by_delivery_id:
type:
- string
- "null"
from_email:
type: string
description: |
Bare email address parsed from the `From:` header, with
display name stripped (e.g. `[email protected]`). Falls
back to `sender` (the SMTP envelope MAIL FROM) when the
`From:` header cannot be parsed.
**This is the canonical "who sent this email" field for
most use cases**, including comparing against allowlists,
routing replies, or displaying the sender to a user. Use
`from_header` when you specifically need the display name,
or `sender`/`smtp_mail_from` when you need the SMTP
envelope value (e.g. to follow a bounce).
to_email:
type: string
description: Parsed to address (same as recipient)
from_known_address:
type: boolean
description: |
True when the inbound's sender address has a matching grant
in the org's known-send-addresses list. Advisory: a true
value does not by itself guarantee that a reply will be
accepted by send-mail's gates; the per-send check at send
time remains authoritative.
replies:
type: array
description: |
Sent emails recorded as replies to this inbound, in send
order (ascending). Populated when a customer's send-mail
request carries an `in_reply_to` Message-ID that matches
this inbound's `message_id` in the same org. Includes
attempts that were gate-denied, so the array reflects every
recorded reply attempt regardless of outcome.
items:
$ref: "#/components/schemas/EmailDetailReply"
reply_to_sent_email_id:
type:
- string
- "null"
format: uuid
description: |
The `sent_emails.id` of the outbound this inbound was a
reply to, when resolvable. Set at inbound ingest by
matching the parsed In-Reply-To (or References, as a
fallback) against `sent_emails.message_id` in the same
org. The mirror of `sent_emails.in_reply_to_email_id` for
the inbound side of a thread. NULL when the inbound is
not a threaded reply to one of your sends, when neither
header survived the path through intermediate MTAs, or on
inbound received before this auto-link landed.
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this message belongs to. Inbound and
outbound messages in the same conversation share a
`thread_id`; fetch `/threads/{thread_id}` for the full
ordered thread. Assigned at ingest. NULL on messages
received before threading was enabled (until backfilled).
parsed:
allOf:
- $ref: "#/components/schemas/ParsedEmailData"
description: |
Parsed MIME content (addresses, threading headers,
attachment metadata), matching the `email.parsed` object
on the webhook payload so one parser handles both the
webhook and this endpoint. The top-level `body_text` /
`body_html` fields above are the same values as
`parsed.body_text` / `parsed.body_html`, retained for
backward compatibility.
auth:
allOf:
- $ref: "#/components/schemas/EmailAuth"
description: |
SPF / DKIM / DMARC verdicts computed at ingest, matching
the `email.auth` object on the webhook payload. Use these
to decide how much to trust a message before acting on
instructions it contains.
required:
- id
- sender
- recipient
- status
- domain
- created_at
- received_at
- webhook_attempt_count
- from_email
- to_email
- replies
- parsed
- auth
EmailDetailReply:
type: object
properties:
id:
type: string
format: uuid
description: Sent-email row id.
status:
$ref: "#/components/schemas/SentEmailStatus"
to_address:
type: string
description: Recipient address as recorded on the sent_emails row.
subject:
type:
- string
- "null"
created_at:
type: string
format: date-time
queue_id:
type:
- string
- "null"
description: Outbound relay queue identifier when available.
required:
- id
- status
- to_address
- created_at
EmailAddress:
type: object
description: A parsed RFC 5322 address with optional display name.
properties:
name:
type:
- string
- "null"
description: Display name, when present (e.g. `Alice Example`).
address:
type: string
description: Bare email address (e.g. `[email protected]`).
required:
- address
EmailAttachment:
type: object
description: |
Metadata for one attachment. The bytes are not inline; download
all attachments for a message as a gzipped tarball via
`/emails/{id}/attachments.tar.gz`. `sha256` lets you verify a
specific part after extraction.
properties:
filename:
type:
- string
- "null"
content_type:
type:
- string
- "null"
size_bytes:
type: integer
sha256:
type:
- string
- "null"
part_index:
type: integer
description: Zero-based index of this part within the message.
required:
- size_bytes
ParsedEmailData:
type: object
description: |
Parsed MIME content for an inbound email. Mirrors the
`email.parsed` object on the webhook payload so a single parser
handles both surfaces. `status` is `complete` when parsing
succeeded; on `failed` the body/address/attachment fields are
absent and `error` describes why.
properties:
status:
type: string
enum:
- complete
- failed
body_text:
type:
- string
- "null"
description: Plain-text body. Present when `status` is `complete`.
body_html:
type:
- string
- "null"
description: HTML body. Present when `status` is `complete`.
reply_to:
type:
- array
- "null"
items:
$ref: "#/components/schemas/EmailAddress"
description: Parsed `Reply-To` header addresses.
cc:
type:
- array
- "null"
items:
$ref: "#/components/schemas/EmailAddress"
description: Parsed `Cc` header addresses.
bcc:
type:
- array
- "null"
items:
$ref: "#/components/schemas/EmailAddress"
description: Parsed `Bcc` header addresses (rarely present on inbound).
to_addresses:
type:
- array
- "null"
items:
$ref: "#/components/schemas/EmailAddress"
description: Parsed `To` header addresses.
in_reply_to:
type:
- array
- "null"
items:
type: string
description: Message-IDs from the `In-Reply-To` header.
references:
type:
- array
- "null"
items:
type: string
description: Message-IDs from the `References` header.
attachments:
type: array
items:
$ref: "#/components/schemas/EmailAttachment"
description: Attachment metadata. Empty array when none.
error:
type:
- object
- "null"
description: |
Present (non-null) only when `status` is `failed`. When
present, all three fields are populated, so a consumer can
branch on `code` without defensive null checks.
properties:
code:
type: string
description: Stable failure code (e.g. `PARSE_FAILED`).
message:
type: string
retryable:
type: boolean
required:
- code
- message
- retryable
required:
- status
DkimSignature:
type: object
description: One DKIM signature found on the message, with its verdict.
properties:
domain:
type: string
selector:
type: string
result:
type: string
description: Verification result (e.g. `pass`, `fail`, `none`).
aligned:
type: boolean
description: Whether the signing domain aligns with the From domain (for DMARC).
keyBits:
type:
- integer
- "null"
algo:
type:
- string
- "null"
required:
- domain
- selector
- result
- aligned
EmailAuth:
type: object
description: |
SPF / DKIM / DMARC verdicts computed at ingest. Mirrors the
`email.auth` object on the webhook payload. Field names are
camelCase to match that payload exactly. For messages received
before auth was recorded, the verdicts default to `none`.
properties:
spf:
type: string
description: SPF result (e.g. `pass`, `fail`, `softfail`, `none`).
dmarc:
type: string
description: DMARC result (e.g. `pass`, `fail`, `none`).
dmarcPolicy:
type:
- string
- "null"
description: Published DMARC policy (`none`, `quarantine`, `reject`).
dmarcFromDomain:
type:
- string
- "null"
description: The From-header domain DMARC was evaluated against.
dmarcSpfAligned:
type: boolean
dmarcDkimAligned:
type: boolean
dmarcSpfStrict:
type:
- boolean
- "null"
dmarcDkimStrict:
type:
- boolean
- "null"
dkimSignatures:
type: array
items:
$ref: "#/components/schemas/DkimSignature"
required:
- spf
- dmarc
- dmarcSpfAligned
- dmarcDkimAligned
- dkimSignatures
Thread:
type: object
description: |
A conversation thread: its metadata plus the inbound and
outbound messages that belong to it, interleaved oldest-first.
Membership is the stored `thread_id` on each message. Bodies are
omitted here to keep the thread view lightweight; fetch
`/emails/{id}` or `/sent-emails/{id}` for a single message's
full content.
properties:
id:
type: string
format: uuid
subject:
type:
- string
- "null"
description: Normalized subject of the thread (Re/Fwd prefixes stripped).
root_message_id:
type:
- string
- "null"
description: Message-ID of the conversation root, when known.
message_count:
type: integer
description: |
Total messages in the thread. `messages` is capped (most
recent first, then re-sorted oldest-first), so
`message_count > messages.length` signals truncation.
first_message_at:
type:
- string
- "null"
format: date-time
last_message_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
messages:
type: array
items:
$ref: "#/components/schemas/ThreadMessage"
required:
- id
- message_count
- created_at
- messages
ThreadMessage:
type: object
description: One message in a thread (inbound or outbound).
properties:
direction:
type: string
enum:
- inbound
- outbound
description: |
`inbound` for a received email (`/emails/{id}`), `outbound`
for a send (`/sent-emails/{id}`). Use it with `id` to fetch
full content from the right endpoint.
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
from:
type:
- string
- "null"
to:
type:
- string
- "null"
subject:
type:
- string
- "null"
status:
type:
- string
- "null"
description: Lifecycle status (an EmailStatus or SentEmailStatus value, per `direction`).
timestamp:
type:
- string
- "null"
format: date-time
description: received_at for inbound, created_at for outbound.
required:
- direction
- id
Conversation:
type: object
description: |
The full conversation an inbound email belongs to, as ordered,
ready-to-prompt turns with bodies. Resolves the thread from the
email and returns every message oldest-first, so an agent that
received an email can pass `messages` straight to a chat model in
one call.
properties:
thread_id:
type:
- string
- "null"
format: uuid
description: |
The thread this email belongs to, or null when the email
isn't threaded yet (the conversation is then just this one
message).
subject:
type:
- string
- "null"
description: |
Normalized thread subject (Re/Fwd prefixes stripped), or the
email's own subject when it isn't threaded.
message_count:
type: integer
description: |
Total messages in the thread. `messages` is capped, so
`truncated` is true (and this can exceed `messages.length`)
when older messages were omitted.
truncated:
type: boolean
description: |
True when `messages` omits part of the conversation because
the thread exceeds the per-call cap.
messages:
type: array
items:
$ref: "#/components/schemas/ConversationMessage"
required:
- thread_id
- message_count
- truncated
- messages
ConversationMessage:
type: object
description: One message in the conversation, with its body and a chat role.
properties:
role:
type: string
enum:
- user
- assistant
description: |
Chat role derived from `direction`: `user` for inbound
(received) messages, `assistant` for outbound (your own prior
replies). Lets `messages` be passed directly to a chat model.
direction:
type: string
enum:
- inbound
- outbound
description: |
`inbound` for a received email (`/emails/{id}`), `outbound`
for a send (`/sent-emails/{id}`).
id:
type: string
format: uuid
message_id:
type:
- string
- "null"
from:
type:
- string
- "null"
to:
type:
- string
- "null"
subject:
type:
- string
- "null"
text:
type: string
description: |
Plain-text body. Empty string when the message has no text
part or its content was discarded by retention.
timestamp:
type:
- string
- "null"
format: date-time
description: received_at for inbound, created_at for outbound.
required:
- role
- direction
- id
- text
SendMailAttachment:
type: object
additionalProperties: false
properties:
filename:
type: string
minLength: 1
maxLength: 255
description: Attachment filename. Control characters are rejected.
content_type:
type: string
minLength: 1
maxLength: 255
description: Optional MIME content type. Control characters are rejected.
content_base64:
type: string
minLength: 1
maxLength: 44040192
description: Base64-encoded attachment bytes.
required:
- filename
- content_base64
SendMailInput:
type: object
additionalProperties: false
properties:
from:
type: string
minLength: 3
maxLength: 998
description: RFC 5322 From header. The sender domain must be a verified outbound domain for your organization.
to:
type: string
minLength: 3
maxLength: 320
description: Recipient address. Recipient eligibility depends on your account's outbound entitlements.
subject:
type: string
minLength: 1
maxLength: 998
description: Subject line for the outbound message
body_text:
type: string
description: Plain-text message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
body_html:
type: string
description: HTML message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes.
in_reply_to:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
description: Message-ID of the direct parent email when sending a threaded reply.
references:
type: array
maxItems: 100
description: Full ordered message-id chain for the thread.
items:
type: string
minLength: 1
maxLength: 998
pattern: ^[^\x00-\x1F\x7F]+$
attachments:
type: array
maxItems: 100
description: Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.
items:
$ref: "#/components/schemas/SendMailAttachment"
wait:
type: boolean
description: When true, wait for the first downstream SMTP delivery outcome before returning.
wait_timeout_ms:
type: integer
minimum: 1000
maximum: 30000
description: Maximum time to wait for a delivery outcome when wait is true. Defaults to 30000.
required:
- from
- to
- subject
EmailStatus:
type: string
description: |
Lifecycle status of an INBOUND email (a row in the `emails`
table). Distinct from `SentEmailStatus`, which describes
the OUTBOUND lifecycle (the `sent_emails` table) and uses
a different vocabulary because the lifecycles differ.
Possible values:
- `pending`: the row was inserted at ingestion (mx_main)
and has not yet completed the spam / filter / auth
pipeline. Body and parsed fields are present; webhook
delivery is not yet scheduled. Most rows transition out
of `pending` within seconds.
- `accepted`: the inbound passed the policy gates and is
queued for webhook delivery. The `webhook_status` field
tracks the separate webhook-delivery lifecycle from
this point.
- `completed`: terminal success. Webhook delivery
attempted and acknowledged by every active endpoint, OR
no endpoints are configured, so the row is durably
archived.
- `rejected`: terminal failure at ingestion (spam, blocked
sender, filter rule, malformed). The body and metadata
are stored for auditing but no webhook fires and the
row is not repliable.
See also `webhook_status` (separate enum tracking the
webhook-delivery state machine) and `SentEmailStatus` (the
outbound vocabulary).
enum:
- pending
- accepted
- completed
- rejected
EmailWebhookStatus:
type:
- string
- "null"
description: |
Webhook-delivery state for an inbound email. Tracks a
SEPARATE lifecycle from the email's `status` field; the
same row carries both. Possible values:
- `pending`: ingestion is past `pending` (the email itself
is `accepted`) but the webhook fan-out has not yet
started for this row.
- `in_flight`: at least one delivery attempt is in flight.
- `fired`: terminal success. Every active endpoint
acknowledged the delivery (or accepted it after retries).
- `failed`: terminal partial-failure. At least one endpoint
exhausted its retry budget; some endpoints may still
have succeeded.
- `exhausted`: terminal failure. Every endpoint exhausted
its retry budget without success.
- `null`: no endpoints configured, so no webhook lifecycle
applies.
Note that the value `pending` here does NOT mean the email
is `pending`; it means the email is past ingestion but
webhook delivery has not yet begun. Two overlapping uses
of the word `pending` for distinct lifecycle phases.
enum:
- pending
- in_flight
- fired
- failed
- exhausted
- null
SentEmailStatus:
type: string
description: |
Lifecycle status of a sent_emails row. Possible values:
- `queued`: pre-call INSERT; the outbound agent has not
yet replied.
- `submitted_to_agent`: agent accepted; `queue_id` is set.
- `agent_failed`: agent rejected; `error_code` and
`error_message` carry the reason.
- `gate_denied`: a recipient-scope gate denied the send;
the agent was never called. The `gates` array carries
the denial detail. /send-mail returns 403 in this case
so callers see the denial synchronously; /sent-emails
additionally records the row for historical lookup,
which is when this status appears in a listing.
- `unknown`: terminal indeterminate; the on-box log
poller couldn't classify the receiver's response.
- `delivered` / `bounced` / `deferred` / `wait_timeout`:
terminal delivery outcomes (see DeliveryStatus).
enum:
- queued
- submitted_to_agent
- agent_failed
- gate_denied
- unknown
- delivered
- bounced
- deferred
- wait_timeout
DeliveryStatus:
type: string
description: |
Narrower enum covering only the four terminal delivery
outcomes returned to a synchronous `wait: true` send.
On the SendMailResult shape, `delivery_status` is always
equal to `status` whenever both are present (i.e. on
terminal-state replays and live wait=true responses).
The two fields exist so callers that want to type-narrow
on "this is a delivery outcome" can pattern-match against
the four-value enum without handling the broader
SentEmailStatus value set (which also covers `queued`,
`submitted_to_agent`, `agent_failed`, `gate_denied`,
`unknown`).
On async-mode and pre-terminal responses, `delivery_status`
is absent and only `status` is populated. Use `status` if
you want a single field that's always present.
enum:
- delivered
- bounced
- deferred
- wait_timeout
SentEmailSummary:
type: object
description: |
List-row projection of a sent-email record. Drops
`body_text` and `body_html` to keep paginated responses
small; fetch /sent-emails/{id} for the full record with
bodies.
properties:
id:
type: string
format: uuid
status:
$ref: "#/components/schemas/SentEmailStatus"
status_changed_at:
type: string
format: date-time
description: |
Timestamp of the most recent status transition.
Polling clients should treat `status='queued'` AND
`status_changed_at` older than 5 minutes as
"stuck-queued" (the post-tx UPDATE failed and the
actual delivery state is recoverable from on-box logs
via `queue_id` when populated, or `request_id`).
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
client_idempotency_key:
type:
- string
- "null"
description: |
Effective idempotency key used for this send. If the
caller passed the `Idempotency-Key` header, this is
that value; otherwise it's a server-derived hash of
the canonical request payload.
content_hash:
type: string
description: Stable hash of the canonical send payload.
from_header:
type: string
description: |
Raw `From:` header as sent on the wire, including any
display name (e.g. `"Acme Support" `).
from_address:
type: string
description: Bare email address parsed from `from_header`.
to_header:
type: string
description: |
Raw `To:` header as sent on the wire, including any
display name.
to_address:
type: string
description: Bare email address parsed from `to_header`.
subject:
type: string
body_size_bytes:
type: integer
description: |
Total UTF-8 byte length of `body_text` + `body_html`.
Surfaced on the list endpoint so callers can see "this
row has a 4MB body" without fetching it.
content_discarded_at:
type:
- string
- "null"
format: date-time
description: |
Timestamp at which the bodies were discarded by an
entitlement-driven retention policy. Null when bodies
are still present. The detail endpoint returns
null-valued `body_text`/`body_html` for discarded rows.
message_id:
type:
- string
- "null"
description: |
Wire-level Message-ID assigned to the outbound message
(RFC 5322). Null on rows that never reached signing
(queued, gate_denied, agent_failed before signing).
in_reply_to:
type:
- string
- "null"
description: |
Wire-level In-Reply-To header value, when this send
was a reply.
email_references:
type:
- string
- "null"
description: |
Wire-level References header value, when this send
was a reply.
in_reply_to_email_id:
type:
- string
- "null"
format: uuid
description: |
Reference to the inbound `emails.id` that this send
replied to, when known. Populated when the caller used
/emails/{id}/reply or when /send-mail's `in_reply_to`
matched a stored inbound message_id in the same org.
thread_id:
type:
- string
- "null"
format: uuid
description: |
Conversation thread this send belongs to. A reply inherits
the thread of the inbound it answers; a fresh send starts a
new thread. Fetch `/threads/{thread_id}` for the full
ordered thread (inbound + outbound interleaved). NULL on
gate-denied sends and on sends created before threading was
enabled.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's outbound
relay once the agent accepts the message. Null on
queued, gate_denied, and agent_failed rows.
smtp_response_code:
type:
- integer
- "null"
description: |
Receiver's 3-digit SMTP code (e.g. 250, 550, 451).
Populated on terminal delivery statuses; may be null
on a deferred where the agent never got an SMTP-level
response (TCP refused, DNS failed, TLS handshake
failed). `smtp_response_text` still carries Postfix's
descriptive text in those cases.
smtp_response_text:
type:
- string
- "null"
description: |
Free-form text portion of the receiver's SMTP
response. The most useful debugging signal on a
`bounced` or `deferred` row.
smtp_enhanced_status_code:
type:
- string
- "null"
description: |
RFC 3463 enhanced status code (e.g. `5.1.1` for "Bad
destination mailbox address"). Distinct from
`smtp_response_code`: the basic 3-digit code is coarse
(550 = "permanent failure"), the enhanced code is
finer-grained.
dkim_selector:
type:
- string
- "null"
description: |
DKIM selector used to sign the outbound message.
Public DNS data; useful for diagnosing why a downstream
verifier rejected the signature.
dkim_domain:
type:
- string
- "null"
description: DKIM signing domain.
error_code:
type:
- string
- "null"
description: |
Stable public error code on `agent_failed` rows. The
agent's internal codes are remapped to a stable public
taxonomy (see `publicAgentError` in the server) so this
field is safe to branch on across agent versions.
error_message:
type:
- string
- "null"
description: Free-form error message accompanying `error_code`.
gates:
type:
- array
- "null"
items:
$ref: "#/components/schemas/GateDenial"
description: |
Gate-denial detail on `gate_denied` rows. Mirrors the
synchronous /send-mail 403 contract so a caller's
GateDenial handler is the same across live denies and
historical lookups. Null on every other status.
request_id:
type:
- string
- "null"
description: |
Server-issued request identifier from the original
/send-mail call. Surfaced as the `X-Request-Id`
response header on the live send and recorded here
for support escalation.
required:
- id
- status
- status_changed_at
- created_at
- updated_at
- content_hash
- from_header
- from_address
- to_header
- to_address
- subject
- body_size_bytes
SemanticSearchField:
type: string
enum:
- subject
- headers
- addresses
- body
description: A searchable email field.
SemanticSearchInput:
type: object
properties:
query:
type: string
minLength: 1
maxLength: 2048
description: |
Free-text query. Required for `semantic` and `hybrid` modes;
optional for `keyword` mode.
mode:
type: string
enum:
- hybrid
- semantic
- keyword
default: hybrid
description: |
Ranking strategy. `keyword` is lexical only, `semantic` is
embedding-based, `hybrid` blends both.
corpus:
type: array
items:
type: string
enum:
- inbound
- outbound
minItems: 1
maxItems: 2
description: |
Which mail to search. Defaults to both received (`inbound`)
and sent (`outbound`).
search_in:
type: array
items:
$ref: "#/components/schemas/SemanticSearchField"
description: Restrict matching to these fields. Defaults to all.
exclude:
type: array
items:
$ref: "#/components/schemas/SemanticSearchField"
description: Exclude these fields from matching.
date_from:
type: string
format: date-time
description: Only include mail at or after this timestamp.
date_to:
type: string
format: date-time
description: Only include mail at or before this timestamp.
include:
type: array
items:
type: string
enum:
- coverage
description: |
Opt-in extras. `coverage` adds an index-coverage snapshot to
`meta`. Matched fields, snippets, and the score breakdown are
always returned regardless of this field.
limit:
type: integer
minimum: 1
maximum: 100
default: 10
description: Maximum number of results to return.
cursor:
type: string
description: Opaque pagination cursor from a prior response's `meta.cursor`.
SemanticSearchSnippet:
type: object
properties:
field:
type: string
description: The field this excerpt came from.
text:
type: string
description: Plain-text excerpt centered on the match (no markup).
required:
- field
- text
SemanticSearchScoreBreakdown:
type: object
description: |
Additive contributions to `score`. `semantic` and `keyword` are the
raw signals times the mode's weight (null when not applicable);
these plus `field_boost` and `recency` sum to `score` before each
value is independently rounded to 5 decimal places.
properties:
semantic:
type:
- number
- "null"
keyword:
type:
- number
- "null"
field_boost:
type: number
recency:
type: number
required:
- semantic
- keyword
- field_boost
- recency
SemanticSearchResult:
type: object
properties:
source_type:
type: string
enum:
- inbound_email
- sent_email
description: Whether this row is a received or sent message.
id:
type: string
description: Message id. Combine with `api_url` to fetch the full record.
subject:
type:
- string
- "null"
from:
type:
- string
- "null"
to:
type:
- string
- "null"
timestamp:
type: string
description: Message timestamp (received_at for inbound, created_at for sent).
status:
type: string
description: Lifecycle status of the message.
score:
type: number
description: Overall relevance score; the `score_breakdown` components account for it.
semantic_score:
type:
- number
- "null"
description: Raw semantic similarity signal, or null when not applicable.
keyword_score:
type:
- number
- "null"
description: Raw keyword (lexical) signal, or null when not applicable.
matched_fields:
type: array
items:
$ref: "#/components/schemas/SemanticSearchField"
description: Fields where the query matched.
snippets:
type: array
items:
$ref: "#/components/schemas/SemanticSearchSnippet"
description: Match-centered excerpts, one per matched field.
score_breakdown:
$ref: "#/components/schemas/SemanticSearchScoreBreakdown"
api_url:
type:
- string
- "null"
description: Relative API path to fetch the full message.
required:
- source_type
- id
- subject
- from
- to
- timestamp
- status
- score
- semantic_score
- keyword_score
- matched_fields
- snippets
- score_breakdown
- api_url
SemanticSearchCoverage:
type: object
description: Index-coverage snapshot for the org, returned only when the `coverage` include option is requested.
properties:
embedded_chunks:
type: integer
pending_chunks:
type: integer
skipped_plan_chunks:
type: integer
skipped_quota_chunks:
type: integer
unsupported_attachment_chunks:
type: integer
failed_chunks:
type: integer
required:
- embedded_chunks
- pending_chunks
- skipped_plan_chunks
- skipped_quota_chunks
- unsupported_attachment_chunks
- failed_chunks
SemanticSearchMeta:
type: object
properties:
limit:
type: integer
description: Page size used for this request.
cursor:
type:
- string
- "null"
description: Cursor for the next page, or null if there are no more results.
mode:
type: string
enum:
- hybrid
- semantic
- keyword
description: Ranking mode used for this response.
coverage:
oneOf:
- $ref: "#/components/schemas/SemanticSearchCoverage"
- type: "null"
description: |
Index-coverage snapshot, present only when requested via
`include: [coverage]`; otherwise null.
required:
- limit
- cursor
- mode
- coverage
SentEmailDetail:
description: |
Full sent-email record, including `body_text` and
`body_html`. Returned by /sent-emails/{id}.
allOf:
- $ref: "#/components/schemas/SentEmailSummary"
- type: object
properties:
body_text:
type:
- string
- "null"
description: |
Plain-text body sent on the wire. Null when the
send carried only an HTML body, or when bodies have
been discarded post-send (`content_discarded_at`
set).
body_html:
type:
- string
- "null"
description: |
HTML body sent on the wire. Null when the send
carried only a plain-text body, or when bodies
have been discarded post-send.
ReplyInput:
type: object
additionalProperties: false
description: |
Body shape for `/emails/{id}/reply`. Intentionally narrow:
recipients (`to`), subject, and threading headers
(`in_reply_to`, `references`) are derived server-side from
the inbound row referenced by the path id and are rejected by
`additionalProperties` if passed (returns 400).
`from` IS allowed because of legitimate use cases (display-name
addition, replying from a different verified outbound address,
multi-team triage). Send-mail's per-send `canSendFrom` gate
validates the from-domain regardless, so the override carries
no extra privilege.
properties:
body_text:
type: string
description: Plain-text reply body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes (same cap as send-mail).
body_html:
type: string
description: HTML reply body. At least one of body_text or body_html is required.
from:
type: string
minLength: 3
maxLength: 998
description: |
Optional override for the reply's From header. Defaults to
the inbound's recipient. Use to add a display name (`"Acme
Support" `) or to reply from a different
verified outbound address (e.g. multi-team routing where
support@ triages to billing@). The from-domain must be a
verified outbound domain for your org, same as send-mail.
wait:
type: boolean
description: When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics.
attachments:
type: array
maxItems: 100
description: Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.
items:
$ref: "#/components/schemas/SendMailAttachment"
SendMailResult:
type: object
properties:
id:
type: string
description: Persisted sent-email attempt ID.
status:
$ref: "#/components/schemas/SentEmailStatus"
from:
type: string
description: |
Bare from-address actually written on the wire. Echoed
on every success branch so callers can confirm what
went out, particularly useful for the /emails/{id}/reply
path where `from` is server-derived from the inbound's
recipient when the caller doesn't override.
For sends where the caller passed a from-header that
included a display name (e.g. `"Acme Support" `),
this field is the parsed bare address (`[email protected]`).
The display name was sent on the wire intact; this field
just makes the address easy to compare against allowlists.
queue_id:
type:
- string
- "null"
description: |
Message identifier assigned by Primitive's OUTBOUND relay
(the box that signs your mail and submits it to the
receiving MTA). NOT the receiver's queue id.
The receiver may also report its own queue id in
`smtp_response_text` (e.g. `"250 2.0.0 Ok: queued as
99D111927CDA"` from a Postfix receiver). Those two ids
refer to different mail systems and are NOT comparable.
Treat `queue_id` as Primitive-internal and the
receiver's id as remote-system-internal.
Null on rows that never reached the relay (queued,
gate_denied, agent_failed before signing).
accepted:
type: array
items:
type: string
description: Recipient addresses accepted by the relay.
rejected:
type: array
items:
type: string
description: Recipient addresses rejected by the relay.
client_idempotency_key:
type: string
description: Effective idempotency key used for this send.
request_id:
type: string
description: Server-issued request identifier for support and tracing.
content_hash:
type: string
description: Stable hash of the canonical send payload.
delivery_status:
$ref: "#/components/schemas/DeliveryStatus"
smtp_response_code:
type:
- integer
- "null"
description: SMTP response code from the first downstream delivery outcome when wait is true.
smtp_response_text:
type: string
description: SMTP response text from the first downstream delivery outcome when wait is true.
idempotent_replay:
type: boolean
description: |
True when the response replays a previously-recorded send
keyed by `client_idempotency_key` (same key, same canonical
payload). False on a fresh send and on gate-denied
responses. Lets callers branch on cache state without
diffing fields.
required:
- id
- status
- from
- queue_id
- accepted
- rejected
- client_idempotency_key
- request_id
- content_hash
- idempotent_replay
SendPermissionRule:
description: |
One recipient-scope rule describing a destination the caller
may send to. Discriminated on `type`. Each rule carries a
human-prose `description` field intended for display.
Rule kinds are stable within an SDK release. A response
containing a `type` value not enumerated in this schema
means the server is running a newer version than the SDK;
upgrade the SDK to the release that matches the server's
schema. Strict-parsing SDKs (Go, Python) will raise a
decode error in that case rather than silently dropping
the unknown rule, since silent drops would let an outbound
agent reason from an incomplete view of its own permissions.
discriminator:
propertyName: type
mapping:
any_recipient: "#/components/schemas/SendPermissionAnyRecipient"
managed_zone: "#/components/schemas/SendPermissionManagedZone"
your_domain: "#/components/schemas/SendPermissionYourDomain"
address: "#/components/schemas/SendPermissionAddress"
oneOf:
- $ref: "#/components/schemas/SendPermissionAnyRecipient"
- $ref: "#/components/schemas/SendPermissionManagedZone"
- $ref: "#/components/schemas/SendPermissionYourDomain"
- $ref: "#/components/schemas/SendPermissionAddress"
SendPermissionAnyRecipient:
type: object
description: |
The caller can send to any recipient. When this rule is
present, every other rule in the response is redundant.
properties:
type:
type: string
enum:
- any_recipient
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- description
SendPermissionManagedZone:
type: object
description: |
The caller can send to any address at the named
Primitive-managed zone. Always emitted (no entitlement
required) because Primitive owns the zone and every mailbox
belongs to a Primitive customer by construction.
properties:
type:
type: string
enum:
- managed_zone
zone:
type: string
description: |
The managed apex domain. Sends are accepted to any
address at the apex itself or any subdomain (e.g.
`[email protected]` and `[email protected]`
both match the `primitive.email` zone rule).
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- zone
- description
SendPermissionYourDomain:
type: object
description: |
The caller can send to any address at one of their own
verified outbound domains. Emitted once per active row in
the org's `domains` table.
properties:
type:
type: string
enum:
- your_domain
domain:
type: string
description: A verified outbound domain owned by the caller's org.
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- domain
- description
SendPermissionAddress:
type: object
description: |
The caller can send to a specific address that has
authenticated inbound mail to the org. Emitted once per row
in the org's `known_send_addresses` table, capped at
`meta.address_cap`.
properties:
type:
type: string
enum:
- address
address:
type: string
description: The bare email address this rule grants sends to.
last_received_at:
type: string
format: date-time
description: |
Most recent inbound email from this address that
authenticated successfully (DMARC pass + DKIM/SPF
alignment). Updated on each new authenticated receipt.
received_count:
type: integer
description: |
Total number of authenticated inbound emails from this
address. Increments only when `last_received_at` advances.
description:
type: string
description: Human-prose summary of the rule.
required:
- type
- address
- last_received_at
- received_count
- description
SendPermissionsMeta:
type: object
description: |
Response metadata for /send-permissions. The `address_cap`
bounds the size of the `address` rule subset; orgs with more
than `address_cap` known addresses almost always also hold a
broader rule type (`any_recipient` or `your_domain`), so the
cap is a response-size bound rather than a meaningful
product limit.
properties:
address_cap:
type: integer
description: Maximum number of `address` rules included in `data`.
truncated:
type: boolean
description: |
True when the org has more than `address_cap` known
addresses and the list was truncated. False when every
known address is represented or when the org holds no
address rules at all.
required:
- address_cap
- truncated
Endpoint:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
url:
type:
- string
- "null"
enabled:
type: boolean
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict this endpoint to emails from a specific domain
rules:
type: object
description: Endpoint-specific filtering rules
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
delivery_count:
type: integer
description: Total webhook deliveries attempted
success_count:
type: integer
description: Successful deliveries
failure_count:
type: integer
description: Failed deliveries
consecutive_fails:
type: integer
description: Current streak of consecutive failures
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
deactivated_at:
type:
- string
- "null"
format: date-time
kind:
type: string
enum:
- http
- function
description: "http: deliver to the webhook URL. function: invoke a Primitive Function."
function_id:
type:
- string
- "null"
format: uuid
description: The Function this endpoint invokes, when kind is function.
is_route_target:
type: boolean
description: |
When true, this endpoint is reachable only via an explicit recipient
route, never as a domain's default destination, and is exempt from
the one-endpoint-per-domain rule (so many can share a domain).
required:
- id
- org_id
- enabled
- rules
- created_at
- updated_at
- delivery_count
- success_count
- failure_count
- consecutive_fails
CreateEndpointInput:
type: object
additionalProperties: false
properties:
kind:
type: string
enum:
- http
- function
default: http
description: "http: deliver to a webhook URL (provide url). function: invoke a Primitive Function (provide function_id, omit url)."
url:
type: string
minLength: 1
description: The webhook URL to deliver events to. Required when kind is http; omit for function endpoints.
function_id:
type: string
format: uuid
description: The Function to invoke. Required when kind is function.
enabled:
type: boolean
default: true
description: Whether the endpoint is active
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict to emails from a specific domain
rules:
type: object
description: Endpoint-specific filtering rules
is_route_target:
type: boolean
default: false
description: |
Create this endpoint as a route-target: reachable only via an
explicit recipient route, never a domain's default destination, and
exempt from the one-endpoint-per-domain rule.
UpdateEndpointInput:
type: object
additionalProperties: false
properties:
url:
type: string
minLength: 1
description: New webhook URL (triggers endpoint rotation)
enabled:
type: boolean
domain_id:
type:
- string
- "null"
format: uuid
rules:
type: object
minProperties: 1
TestResult:
type: object
properties:
status:
type: integer
description: HTTP status code returned by the endpoint
body:
type: string
description: Response body (truncated to 1000 characters)
signature:
type: string
description: The signature header value sent (if webhook secret is configured)
required:
- status
- body
Filter:
type: object
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: If set, filter applies only to this domain
type:
type: string
enum:
- whitelist
- blocklist
pattern:
type: string
description: Email address or pattern to match (stored lowercase)
enabled:
type: boolean
created_at:
type: string
format: date-time
required:
- id
- org_id
- type
- pattern
- enabled
- created_at
CreateFilterInput:
type: object
additionalProperties: false
properties:
type:
type: string
enum:
- whitelist
- blocklist
pattern:
type: string
minLength: 1
maxLength: 500
description: Email address or pattern to filter
domain_id:
type:
- string
- "null"
format: uuid
description: Restrict filter to a specific domain (Pro plan required)
required:
- type
- pattern
UpdateFilterInput:
type: object
additionalProperties: false
properties:
enabled:
type: boolean
required:
- enabled
WakeSchedule:
type: object
description: A cron schedule that sends a wake.dispatch command to a function.
properties:
id:
type: string
format: uuid
from_address:
type:
- string
- "null"
description: The sending identity the wake is signed as.
target_address:
type: string
description: The function address the wake is delivered to.
command:
type: string
args:
type: object
additionalProperties: true
cron_expr:
type: string
description: 5-field cron expression.
timezone:
type: string
description: IANA timezone the cron is evaluated in.
next_run_at:
type: string
format: date-time
last_run_at:
type:
- string
- "null"
format: date-time
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- target_address
- command
- cron_expr
- timezone
- next_run_at
- enabled
- created_at
- updated_at
CreateWakeScheduleInput:
type: object
additionalProperties: false
properties:
from_address:
type: string
description: Sending identity (must be a domain the org can sign).
target_address:
type: string
description: Your function address (must differ from from_address).
command:
type: string
minLength: 1
maxLength: 200
args:
type: object
additionalProperties: true
description: Optional JSON object passed through to the woken function.
cron_expr:
type: string
minLength: 1
maxLength: 120
timezone:
type: string
minLength: 1
maxLength: 64
default: UTC
note:
type: string
maxLength: 2000
required:
- from_address
- target_address
- command
- cron_expr
UpdateWakeScheduleInput:
type: object
additionalProperties: false
properties:
enabled:
type: boolean
command:
type: string
minLength: 1
maxLength: 200
args:
type: object
additionalProperties: true
cron_expr:
type: string
minLength: 1
maxLength: 120
timezone:
type: string
minLength: 1
maxLength: 64
from_address:
type: string
target_address:
type: string
note:
type:
- string
- "null"
maxLength: 2000
WakeAuthorization:
type: object
description: A per-target allowlist grant authorizing a sender to wake a function.
properties:
id:
type: string
format: uuid
recipient_endpoint_id:
type: string
format: uuid
allowed_sender_domain:
type: string
allowed_sender_address:
type:
- string
- "null"
allowed_commands:
type:
- array
- "null"
items:
type: string
enabled:
type: boolean
note:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- recipient_endpoint_id
- allowed_sender_domain
- enabled
- created_at
CreateWakeAuthorizationInput:
type: object
additionalProperties: false
properties:
recipient_endpoint_id:
type: string
format: uuid
allowed_sender_domain:
type: string
minLength: 1
maxLength: 253
description: Fully-qualified sender domain (at least two labels).
allowed_sender_address:
type:
- string
- "null"
description: Optional specific sender address to pin the grant to.
allowed_commands:
type:
- array
- "null"
maxItems: 64
items:
type: string
minLength: 1
maxLength: 200
description: Optional command allowlist; null = any command.
note:
type: string
maxLength: 2000
required:
- recipient_endpoint_id
- allowed_sender_domain
UpdateWakeAuthorizationInput:
type: object
additionalProperties: false
properties:
enabled:
type: boolean
required:
- enabled
WakeDispatch:
type: object
description: A recorded wake.dispatch interaction (audit row).
properties:
id:
type: string
format: uuid
wire_id:
type: string
role:
type: string
state:
type: string
outcome:
type:
- string
- "null"
awaiting:
type:
- string
- "null"
counterparty_address:
type: string
our_address:
type: string
step_count:
type: integer
created_at:
type: string
format: date-time
completed_at:
type:
- string
- "null"
format: date-time
required:
- id
- wire_id
- role
- state
- counterparty_address
- our_address
- created_at
RecipientRoute:
type: object
description: A recipient routing rule binding an address pattern to one endpoint.
properties:
id:
type: string
format: uuid
org_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
description: Domain the route is scoped to; null = org-wide.
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
description: The recipient address pattern (an exact address or a wildcard).
pattern_norm:
type:
- string
- "null"
description: Normalized pattern used for matching.
endpoint_id:
type: string
format: uuid
description: The endpoint inbound mail matching this rule is delivered to.
priority:
type: integer
description: Evaluation order within a scope; lower is checked first.
enabled:
type: boolean
match_count:
type: string
description: How many emails have matched this rule (a bigint, returned as a string).
last_matched_at:
type:
- string
- "null"
format: date-time
created_at:
type: string
format: date-time
required:
- id
CreateRouteInput:
type: object
additionalProperties: false
description: |
Provide exactly one of `endpoint_id` or `function_id`. With `function_id`,
a route-target endpoint is minted for that function and the route is bound
to it in one transaction.
properties:
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
minLength: 1
maxLength: 512
endpoint_id:
type: string
format: uuid
description: An existing endpoint to route to. Mutually exclusive with function_id.
function_id:
type: string
format: uuid
description: Route to this function, minting its route-target endpoint if needed. Mutually exclusive with endpoint_id.
domain_id:
type:
- string
- "null"
format: uuid
description: Scope the route to a domain; defaults to the pattern's domain.
priority:
type: integer
minimum: 0
maximum: 1000000
enabled:
type: boolean
required:
- match_type
- pattern
UpdateRouteInput:
type: object
additionalProperties: false
properties:
match_type:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
minLength: 1
maxLength: 512
endpoint_id:
type: string
format: uuid
domain_id:
type:
- string
- "null"
format: uuid
priority:
type: integer
minimum: 0
maximum: 1000000
enabled:
type: boolean
ReorderRoutesInput:
type: object
additionalProperties: false
properties:
updates:
type: array
minItems: 1
maxItems: 1000
items:
type: object
additionalProperties: false
properties:
id:
type: string
format: uuid
priority:
type: integer
minimum: 0
maximum: 1000000
required:
- id
- priority
required:
- updates
SimulateRouteInput:
type: object
additionalProperties: false
properties:
recipient:
type: string
minLength: 1
maxLength: 320
event_type:
type: string
minLength: 1
maxLength: 100
description: Event type to model; defaults to email.received.
required:
- recipient
RouteEvaluatedEntry:
type: object
properties:
route_id:
type: string
tier:
type: string
enum:
- exact
- wildcard
- regex
pattern:
type: string
result:
type: string
enum:
- hit
- miss
- skipped
- error
reason:
type: string
required:
- route_id
- tier
- pattern
- result
SimulateRouteResult:
type: object
description: Where an inbound email to the recipient would be delivered, and why.
properties:
outcome:
type: string
enum:
- matched
- defaulted
- none
recipient:
type: string
endpoint_id:
type:
- string
- "null"
matched_route_id:
type:
- string
- "null"
matched_tier:
type:
- string
- "null"
enum:
- exact
- wildcard
- regex
- null
matched_pattern:
type:
- string
- "null"
default_scope:
type:
- string
- "null"
enum:
- domain
- org
- null
evaluated:
type: array
items:
$ref: "#/components/schemas/RouteEvaluatedEntry"
truncated:
type: boolean
required:
- outcome
- recipient
- endpoint_id
- matched_route_id
- matched_tier
- matched_pattern
- default_scope
- evaluated
- truncated
DeliverySummary:
type: object
properties:
id:
type: string
description: Delivery ID (numeric string)
email_id:
type: string
format: uuid
org_id:
type: string
format: uuid
endpoint_id:
type: string
format: uuid
endpoint_url:
type: string
status:
type: string
enum:
- pending
- delivered
- header_confirmed
- failed
attempt_count:
type: integer
duration_ms:
type:
- integer
- "null"
last_error:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
email:
type:
- object
- "null"
properties:
sender:
type: string
recipient:
type: string
subject:
type:
- string
- "null"
required:
- sender
- recipient
required:
- id
- email_id
- org_id
- endpoint_id
- endpoint_url
- status
- attempt_count
- created_at
- updated_at
ReplayResult:
type: object
properties:
delivered:
type: integer
description: Number of successful deliveries
failed:
type: integer
description: Number of failed deliveries
required:
- delivered
- failed
DiscardContentResult:
type: object
properties:
discarded:
type: boolean
description: |
Always `true` on a 2xx response. The content is either now
discarded as a result of this call, or was already discarded
before this call ran.
already_discarded:
type: boolean
description: |
`true` if the email's content was already discarded before
this call ran (no work was done). `false` if this call was
the one that performed the discard.
required:
- discarded
- already_discarded
FunctionDeployStatus:
type: string
enum:
- pending
- deployed
- failed
description: |
Lifecycle state of the latest deploy attempt:
* `pending` â deploy in flight; the runtime has not yet
confirmed the new bundle is live.
* `deployed` â the running edge handler is the latest code.
* `failed` â the most recent deploy attempt failed; the
previously-live code (if any) is still running. The
`deploy_error` field carries the error message.
FunctionListItem:
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:
$ref: "#/components/schemas/FunctionDeployStatus"
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
FunctionDetail:
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:
$ref: "#/components/schemas/FunctionDeployStatus"
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
CreateFunctionInput:
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
CreateFunctionResult:
type: object
description: Returned by POST /functions on a successful deploy.
properties:
id:
type: string
format: uuid
name:
type: string
deploy_status:
$ref: "#/components/schemas/FunctionDeployStatus"
required:
- id
- name
- deploy_status
UpdateFunctionInput:
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: []
TestInvocationResult:
type: object
description: |
Metadata returned by POST /functions/{id}/test. The send is
queued; poll `trace_url` to watch the run progress through
send -> inbound -> webhook deliveries -> outbound requests,
logs, and replies.
properties:
test_run_id:
type: string
format: uuid
description: Durable test run id used to fetch the run trace.
inbound_domain:
type: string
description: Verified inbound domain the test email was sent to.
to:
type: string
description: Synthetic local-part plus inbound_domain. Visible in the org's inbox.
from:
type: string
description: Primitive-controlled outbound sender used for the test.
send_id:
type: string
description: |
Outbound message id from the underlying send. NOT the
inbound email's id; the inbound id is created when the
email arrives via MX and lands on the function's
invocations list.
subject:
type: string
description: Subject placed on the test email so it can be located in the inbox.
poll_since:
type: string
format: date-time
description: |
ISO timestamp suitable as a `since` lower bound when
polling /emails for the inbound's arrival. Captured
slightly before the send to absorb light clock skew.
watch_url:
type: string
format: uri
description: Function detail page where invocations show up live.
trace_url:
type: string
description: Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace.
required:
- test_run_id
- inbound_domain
- to
- from
- send_id
- subject
- poll_since
- watch_url
- trace_url
FunctionRouting:
type: object
description: |
A single route binding for a function. `domain` is null when the
binding is the org's fallback (any active domain without a scoped
binding); otherwise it carries the scoped domain. `rules` is
reserved for future routing predicates.
properties:
endpoint_id:
type: string
format: uuid
enabled:
type: boolean
domain:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type:
- string
- "null"
required:
- id
rules:
type: object
description: Future routing predicates. Currently empty.
delivery_count:
type: integer
success_count:
type: integer
failure_count:
type: integer
consecutive_fails:
type: integer
last_delivery_at:
type:
- string
- "null"
format: date-time
last_success_at:
type:
- string
- "null"
format: date-time
last_failure_at:
type:
- string
- "null"
format: date-time
required:
- endpoint_id
- enabled
- domain
- rules
RoutingTopology:
type: object
description: |
Org-wide map of function routing: which domain points at which
function, the org's fallback binding (if any), and every
deployed function with no route currently bound.
properties:
domains:
type: array
items:
type: object
properties:
domain_id:
type: string
format: uuid
domain:
type: string
routed_function:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type: string
required:
- id
- name
endpoint_enabled:
type:
- boolean
- "null"
required:
- domain_id
- domain
- routed_function
- endpoint_enabled
fallback_function:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
name:
type: string
required:
- id
- name
fallback_enabled:
type:
- boolean
- "null"
unrouted_functions:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
required:
- id
- name
required:
- domains
- fallback_function
- fallback_enabled
- unrouted_functions
FunctionRouteBody:
type: object
description: |
Target for a route binding. Either a specific verified domain
(scoped) or the org-wide fallback. Pass `takeover: true` to
deactivate any conflicting binding before installing this one.
properties:
target:
oneOf:
- type: object
properties:
kind:
type: string
enum:
- domain
domainId:
type: string
format: uuid
required:
- kind
- domainId
- type: object
properties:
kind:
type: string
enum:
- fallback
required:
- kind
takeover:
type: boolean
description: When true, deactivate any conflicting binding before installing this one.
required:
- target
FunctionRouteResult:
type: object
description: |
On success, carries the new `routing`. On conflict, carries
`conflict` describing the binding holder so the caller can
re-issue with `takeover: true`.
properties:
routing:
oneOf:
- $ref: "#/components/schemas/FunctionRouting"
- type: "null"
conflict:
type: object
properties:
kind:
type: string
enum:
- http
- function
functionId:
type:
- string
- "null"
format: uuid
functionName:
type:
- string
- "null"
url:
type:
- string
- "null"
required:
- kind
FunctionTestRunState:
type: string
description: |
High-level state for a function test run trace:
- `send_failed`: the initial test email send failed.
- `waiting_for_send`: the test run was created but no send result has been recorded yet.
- `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.
- `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.
- `completed`: the function webhook completed successfully.
- `failed`: webhook delivery exhausted retries.
enum:
- send_failed
- waiting_for_send
- waiting_for_inbound
- waiting_for_function
- completed
- failed
FunctionTestRun:
type: object
properties:
id:
type: string
format: uuid
function_id:
type: string
format: uuid
inbound_domain:
type: string
to:
type: string
from:
type: string
subject:
type: string
poll_since:
type: string
format: date-time
created_at:
type: string
format: date-time
sent_at:
type:
- string
- "null"
format: date-time
send_error:
type:
- string
- "null"
required:
- id
- function_id
- inbound_domain
- to
- from
- subject
- poll_since
- created_at
- sent_at
- send_error
FunctionTestRunSend:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
status:
$ref: "#/components/schemas/SentEmailStatus"
queue_id:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- status
- queue_id
- created_at
- updated_at
FunctionTestRunInboundEmail:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
status:
$ref: "#/components/schemas/EmailStatus"
received_at:
type: string
format: date-time
from:
type: string
to:
type: string
subject:
type:
- string
- "null"
webhook_status:
$ref: "#/components/schemas/EmailWebhookStatus"
webhook_attempt_count:
type: integer
webhook_last_status_code:
type:
- integer
- "null"
webhook_last_error:
type:
- string
- "null"
required:
- id
- status
- received_at
- from
- to
- subject
- webhook_status
- webhook_attempt_count
- webhook_last_status_code
- webhook_last_error
FunctionTestRunDeliveryEndpoint:
type:
- object
- "null"
properties:
id:
type: string
format: uuid
kind:
type: string
description: Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear.
function_id:
type:
- string
- "null"
format: uuid
function_name:
type:
- string
- "null"
domain_id:
type:
- string
- "null"
format: uuid
enabled:
type: boolean
deactivated_at:
type:
- string
- "null"
format: date-time
is_current_function:
type: boolean
required:
- id
- kind
- function_id
- function_name
- domain_id
- enabled
- deactivated_at
- is_current_function
FunctionTestRunDelivery:
type: object
properties:
id:
type: string
description: Webhook delivery id.
endpoint_id:
type: string
format: uuid
endpoint_url:
type: string
format: uri
status:
type: string
enum:
- pending
- delivered
- header_confirmed
- failed
attempt_count:
type: integer
duration_ms:
type:
- integer
- "null"
last_error:
type:
- string
- "null"
last_error_code:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
endpoint:
$ref: "#/components/schemas/FunctionTestRunDeliveryEndpoint"
required:
- id
- endpoint_id
- endpoint_url
- status
- attempt_count
- duration_ms
- last_error
- last_error_code
- created_at
- updated_at
- endpoint
FunctionTestRunOutboundRequest:
type: object
properties:
id:
type: string
format: uuid
function_id:
type: string
format: uuid
webhook_delivery_id:
type:
- string
- "null"
email_id:
type:
- string
- "null"
format: uuid
endpoint_id:
type:
- string
- "null"
format: uuid
method:
type: string
url:
type: string
format: uri
host:
type: string
path:
type: string
status_code:
type:
- integer
- "null"
ok:
type:
- boolean
- "null"
duration_ms:
type: integer
error:
type:
- string
- "null"
ts:
type: string
format: date-time
required:
- id
- function_id
- webhook_delivery_id
- email_id
- endpoint_id
- method
- url
- host
- path
- status_code
- ok
- duration_ms
- error
- ts
FunctionTestRunReply:
type: object
properties:
id:
type: string
format: uuid
status:
$ref: "#/components/schemas/SentEmailStatus"
to:
type: string
subject:
type: string
queue_id:
type:
- string
- "null"
created_at:
type: string
format: date-time
required:
- id
- status
- to
- subject
- queue_id
- created_at
FunctionTestRunTrace:
type: object
description: |
End-to-end trace for a `POST /functions/{id}/test` run. The
shape is stable, but many nested sections are null or empty
until the corresponding phase has happened.
properties:
state:
$ref: "#/components/schemas/FunctionTestRunState"
test_run:
$ref: "#/components/schemas/FunctionTestRun"
test_send:
$ref: "#/components/schemas/FunctionTestRunSend"
inbound_email:
$ref: "#/components/schemas/FunctionTestRunInboundEmail"
deliveries:
type: array
items:
$ref: "#/components/schemas/FunctionTestRunDelivery"
outbound_requests:
type: array
items:
$ref: "#/components/schemas/FunctionTestRunOutboundRequest"
logs:
type: array
items:
$ref: "#/components/schemas/FunctionLogRow"
replies:
type: array
items:
$ref: "#/components/schemas/FunctionTestRunReply"
required:
- state
- test_run
- test_send
- inbound_email
- deliveries
- outbound_requests
- logs
- replies
FunctionLogRow:
type: object
description: |
One row from GET /functions/{id}/logs. Represents a single
captured log line emitted by the running handler (e.g. via
`console.log` / `console.error`).
properties:
id:
type: string
format: uuid
description: Unique log row id (stable across pages).
function_id:
type: string
format: uuid
description: The function this log row belongs to.
level:
type: string
enum:
- debug
- log
- info
- warn
- error
description: |
Severity. `log` is the runtime's default for unannotated
`console.log` calls; the other levels match standard
`console.*` methods.
message:
type: string
description: |
The textual message body. The runtime stringifies non-string
arguments before persisting, so this is always a plain
string.
ts:
type: string
format: date-time
description: |
When the handler emitted this line. Newest-first ordering
on this column drives pagination; clock is the runtime's,
not the gateway's.
metadata:
type:
- object
- "null"
additionalProperties: true
description: |
Optional structured payload the runtime attaches alongside
the message (e.g. extra args passed to `console.log`).
Shape is opaque; treat keys as untyped.
required:
- id
- function_id
- level
- message
- ts
FunctionSecretListItem:
type: object
description: |
One row from GET /functions/{id}/secrets. Discriminate on the
`managed` field:
* `managed = true` â system secret provisioned by Primitive.
`description` is set; `created_at` / `updated_at` are
null because the row is virtual (resolved at deploy time
from the managed registry, not stored in the secrets
table).
* `managed = false` â secret the user set via the API.
`created_at` / `updated_at` are set; `description` is
null.
properties:
key:
type: string
managed:
type: boolean
description: True for managed system secrets, false for user-set entries.
description:
type:
- string
- "null"
description: Set on managed entries only; null on user-set entries.
created_at:
type:
- string
- "null"
format: date-time
description: Set on user-set entries only; null on managed entries.
updated_at:
type:
- string
- "null"
format: date-time
description: Set on user-set entries only; null on managed entries.
required:
- key
- managed
CreateFunctionSecretInput:
type: object
additionalProperties: false
description: Body for POST /functions/{id}/secrets.
properties:
key:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
description: |
Uppercase letters, digits, and underscores. Must start with
a letter or underscore. System-managed keys (e.g.
PRIMITIVE_WEBHOOK_SECRET, PRIMITIVE_API_KEY, and
PRIMITIVE_API_BASE_URL) are reserved.
value:
type: string
minLength: 1
maxLength: 4096
description: |
Secret value, up to 4096 UTF-8 bytes. Encrypted at rest.
Never returned by any read endpoint.
required:
- key
- value
SetFunctionSecretInput:
type: object
additionalProperties: false
description: Body for PUT /functions/{id}/secrets/{key}. Key comes from the path.
properties:
value:
type: string
minLength: 1
maxLength: 4096
required:
- value
FunctionSecretWriteResult:
type: object
description: Returned by POST and PUT secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
OrgSecretListItem:
type: object
description: |
One row from GET /org/secrets. Org secrets are always user-set
(there are no managed org secrets), so `created_at` /
`updated_at` are always present.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- key
- created_at
- updated_at
CreateOrgSecretInput:
type: object
additionalProperties: false
description: Body for POST /org/secrets.
properties:
key:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
description: |
Uppercase letters, digits, and underscores. Must start with
a letter or underscore. System-managed keys are reserved.
value:
type: string
minLength: 1
maxLength: 4096
description: |
Secret value, up to 4096 UTF-8 bytes. Encrypted at rest.
Never returned by any read endpoint.
required:
- key
- value
SetOrgSecretInput:
type: object
additionalProperties: false
description: Body for PUT /org/secrets/{key}. Key comes from the path.
properties:
value:
type: string
minLength: 1
maxLength: 4096
required:
- value
OrgSecretWriteResult:
type: object
description: Returned by POST and PUT org secret routes.
properties:
key:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
created:
type: boolean
description: True if this call inserted a new row, false if it updated an existing one.
required:
- key
- created_at
- updated_at
- created
NumericString:
type: string
pattern: ^[0-9]+$
description: Bigint counter serialized as a base-10 string.
MemoryJsonValue:
description: JSON value accepted by Primitive Memories. The server accepts strings, numbers, booleans, null, arrays, and objects, validates nested values, and rejects values that do not serialize as JSON.
oneOf:
- type: "null"
- type: string
- type: number
- type: boolean
- type: array
items:
$ref: "#/components/schemas/MemoryJsonValue"
- type: object
additionalProperties:
$ref: "#/components/schemas/MemoryJsonValue"
MemoryScope:
description: Memory scope. `org` resolves to the authenticated organization. `function` requires the function id UUID in `id`; function names are not valid scope identifiers. Function-authenticated requests cannot override their own Function scope.
oneOf:
- type: object
additionalProperties: false
properties:
type:
type: string
enum:
- org
required:
- type
- type: object
additionalProperties: false
properties:
type:
type: string
enum:
- function
id:
type: string
format: uuid
description: Function id UUID.
required:
- type
- id
MemoryResolvedScope:
type: object
additionalProperties: false
description: Resolved memory scope returned by the API.
properties:
type:
type: string
enum:
- org
- function
id:
type: string
format: uuid
description: Org id for org scope, function id for function scope.
required:
- type
- id
MemoryRecord:
type: object
additionalProperties: false
description: Metadata for a Primitive memory. Search responses omit `value` when `include_value=false`.
properties:
id:
type: string
format: uuid
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
scope:
$ref: "#/components/schemas/MemoryResolvedScope"
value:
$ref: "#/components/schemas/MemoryJsonValue"
version:
$ref: "#/components/schemas/NumericString"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
last_read_at:
type:
- string
- "null"
format: date-time
description: Last successful get timestamp, or null before any get.
read_count:
$ref: "#/components/schemas/NumericString"
write_count:
$ref: "#/components/schemas/NumericString"
expires_at:
type:
- string
- "null"
format: date-time
description: Expiration timestamp, or null for no TTL.
created_by:
type:
- string
- "null"
description: Actor that created the memory, when available.
updated_by:
type:
- string
- "null"
description: Actor that last updated the memory, when available.
required:
- id
- key
- scope
- version
- created_at
- updated_at
- last_read_at
- read_count
- write_count
- expires_at
- created_by
- updated_by
MemoryRecordWithValue:
type: object
additionalProperties: false
description: Memory record returned by get and set operations.
properties:
id:
type: string
format: uuid
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
scope:
$ref: "#/components/schemas/MemoryResolvedScope"
value:
$ref: "#/components/schemas/MemoryJsonValue"
version:
$ref: "#/components/schemas/NumericString"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
last_read_at:
type:
- string
- "null"
format: date-time
description: Last successful get timestamp, or null before any get.
read_count:
$ref: "#/components/schemas/NumericString"
write_count:
$ref: "#/components/schemas/NumericString"
expires_at:
type:
- string
- "null"
format: date-time
description: Expiration timestamp, or null for no TTL.
created_by:
type:
- string
- "null"
description: Actor that created the memory, when available.
updated_by:
type:
- string
- "null"
description: Actor that last updated the memory, when available.
required:
- id
- key
- scope
- value
- version
- created_at
- updated_at
- last_read_at
- read_count
- write_count
- expires_at
- created_by
- updated_by
SetMemoryInput:
type: object
additionalProperties: false
properties:
key:
type: string
minLength: 1
maxLength: 512
description: Caller-defined key, at most 512 UTF-8 bytes.
value:
$ref: "#/components/schemas/MemoryJsonValue"
scope:
$ref: "#/components/schemas/MemoryScope"
ttl_seconds:
type: integer
minimum: 1
maximum: 31536000
description: Set or replace the TTL in seconds. Mutually exclusive with `expires_at` and `clear_ttl`.
expires_at:
type: string
format: date-time
description: Set or replace the absolute expiration timestamp. Mutually exclusive with `ttl_seconds` and `clear_ttl`.
clear_ttl:
type: boolean
description: Clear any existing TTL. Mutually exclusive with `ttl_seconds` and `expires_at`.
if_absent:
type: boolean
description: Create only when the key is absent. Mutually exclusive with `if_version`.
if_version:
$ref: "#/components/schemas/NumericString"
required:
- key
- value
DeleteMemoryResult:
type: object
additionalProperties: false
properties:
deleted:
type: boolean
key:
type: string
scope:
$ref: "#/components/schemas/MemoryResolvedScope"
required:
- deleted
- key
- scope
TemplateAuthor:
type: object
additionalProperties: false
properties:
id:
type: string
minLength: 1
name:
type: string
minLength: 1
url:
type: string
format: uri
required:
- id
- name
TemplateSource:
oneOf:
- type: object
additionalProperties: false
properties:
mode:
type: string
const: managed-build
dir:
type: string
minLength: 1
default: .
required:
- mode
- dir
- type: object
additionalProperties: false
properties:
mode:
type: string
const: bundle
file:
type: string
minLength: 1
required:
- mode
- file
TemplateInstall:
type: object
additionalProperties: false
properties:
mode:
type: string
enum:
- deploy
- scaffold
editFiles:
type: array
items:
type: string
minLength: 1
default: []
reason:
type: string
default: ""
required:
- mode
- editFiles
- reason
TemplateSecret:
type: object
additionalProperties: false
properties:
key:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
required:
type: boolean
default: true
description:
type: string
required:
- key
- required
TemplateSecretGroup:
type: object
additionalProperties: false
properties:
keys:
type: array
minItems: 2
items:
type: string
pattern: ^[A-Z_][A-Z0-9_]*$
min:
type: integer
minimum: 1
default: 1
description:
type: string
required:
- keys
- min
TemplateVariableValidation:
type: object
additionalProperties: false
properties:
pattern:
type: string
minLength: 1
maxLength:
type: integer
minimum: 1
TemplateVariable:
type: object
additionalProperties: false
properties:
key:
type: string
minLength: 1
prompt:
type: string
minLength: 1
default:
type: string
file:
type: string
minLength: 1
type:
type: string
enum:
- string
- select
- url
- email
default: string
options:
type: array
items:
type: string
minLength: 1
validation:
$ref: "#/components/schemas/TemplateVariableValidation"
required:
- key
- prompt
- type
TemplateSetup:
type: object
additionalProperties: false
properties:
agent:
type: string
minLength: 1
prompt:
type: string
minLength: 1
produces:
type: array
items:
type: string
minLength: 1
default: []
required:
- agent
- prompt
- produces
TemplateVendorConsumption:
type: object
additionalProperties: false
properties:
slug:
type: string
minLength: 1
required:
type: boolean
default: true
required:
- slug
TemplateManifest:
type: object
additionalProperties: false
properties:
schemaVersion:
type: integer
const: 1
id:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Stable template slug from the manifest.
title:
type: string
minLength: 1
summary:
type: string
minLength: 1
description:
type: string
author:
$ref: "#/components/schemas/TemplateAuthor"
tags:
type: array
items:
type: string
minLength: 1
default: []
source:
$ref: "#/components/schemas/TemplateSource"
install:
$ref: "#/components/schemas/TemplateInstall"
secrets:
type: array
items:
$ref: "#/components/schemas/TemplateSecret"
default: []
secretGroups:
type: array
items:
$ref: "#/components/schemas/TemplateSecretGroup"
default: []
variables:
type: array
items:
$ref: "#/components/schemas/TemplateVariable"
default: []
consumesVendors:
type: array
items:
$ref: "#/components/schemas/TemplateVendorConsumption"
default: []
setup:
$ref: "#/components/schemas/TemplateSetup"
postInstall:
type: string
required:
- schemaVersion
- id
- title
- summary
- author
- tags
- source
- install
- secrets
- secretGroups
- variables
TemplateRegistryStatus:
type: string
enum:
- pending
- approved
- rejected
TemplateRegistrySummary:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Stable template slug used in template URLs and install commands.
title:
type: string
summary:
type: string
author:
$ref: "#/components/schemas/TemplateAuthor"
tags:
type: array
items:
type: string
verified:
type: boolean
install_count:
type: integer
minimum: 0
github_repo:
type: string
pattern: ^[A-Za-z0-9-]+/[A-Za-z0-9_.-]+$
description: GitHub repository in owner/repo form.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- id
- slug
- title
- summary
- author
- tags
- verified
- install_count
- github_repo
- created_at
- updated_at
TemplateRegistryDetail:
type: object
properties:
id:
type: string
format: uuid
slug:
type: string
pattern: ^[a-z0-9][a-z0-9_-]{0,62}$
description: Stable template slug used in template URLs and install commands.
title:
type: string
summary:
type: string
author:
$ref: "#/components/schemas/TemplateAuthor"
tags:
type: array
items:
type: string
verified:
type: boolean
install_count:
type: integer
minimum: 0
github_repo:
type: string
pattern: ^[A-Za-z0-9-]+/[A-Za-z0-9_.-]+$
description: GitHub repository in owner/repo form.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
description:
type:
- string
- "null"
github_sha:
type: string
github_path:
type:
- string
- "null"
manifest:
$ref: "#/components/schemas/TemplateManifest"
readme:
type:
- string
- "null"
status:
$ref: "#/components/schemas/TemplateRegistryStatus"
required:
- id
- slug
- title
- summary
- author
- tags
- verified
- install_count
- github_repo
- created_at
- updated_at
- description
- github_sha
- github_path
- manifest
- readme
- status
TemplateRegistryPage:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/TemplateRegistrySummary"
next_cursor:
type:
- string
- "null"
description: Cursor to pass as the next `cursor` query value, or null when there are no more templates.
required:
- items
- next_cursor
headers:
IdempotencyKey:
description: Effective idempotency key for this request. When the caller supplies an `Idempotency-Key` request header the same value is echoed; otherwise the server derives one from the canonical payload hash. Use this to correlate a request to its stored sent-email record via the `idempotency_key` filter on `GET /sent-emails`.
schema:
type: string
minLength: 1
maxLength: 255
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