See More

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