openapi: 3.1.0
info:
title: Primitive Account Agent API
version: 1.0.0
description: "Primitive is email infrastructure for AI agents. The Primitive API lets you manage domains, emails, webhook endpoints,\nfilters, and account settings programmatically.\n\n## Authentication\n\nMost endpoints require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer prim_<your_api_key>\nAuthorization: Bearer prim_oat_<oauth_access_token>\n```\n\nAPI keys and OAuth access tokens are org-scoped. Create and manage them in your dashboard\nunder Settings > API Keys. CLI login plus CLI/agent signup endpoints\nexplicitly declare `security: []`; they do not require an API key because\nthey are used to create OAuth CLI sessions.\n\n## Rate Limiting\n\nThe API enforces a sliding window rate limit of **120 requests per\n60 seconds** per organization. When exceeded, the API returns `429`\nwith a `Retry-After` header indicating how many seconds to wait.\n\n## Pagination\n\nList endpoints use cursor-based pagination. Responses include a\n`meta` object with `total`, `limit`, and `cursor` fields. Pass the\n`cursor` value as a query parameter to fetch the next page. When\n`cursor` is `null`, there are no more results.\n\n## Response Format\n\nAll responses use a consistent envelope:\n\n```json\n{\n \"success\": true,\n \"data\": { ... },\n \"meta\": { \"total\": 42, \"limit\": 50, \"cursor\": \"...\" }\n}\n```\n\nErrors follow the same pattern:\n\n```json\n{\n \"success\": false,\n \"error\": { \"code\": \"not_found\", \"message\": \"Email not found\" }\n}\n```\n\n## Webhook signing\n\nOutbound webhook deliveries (configured via the `endpoints` API)\nare signed so receivers can verify they came from Primitive and\nhave not been tampered with in transit. The signing scheme is\ndeliberately simple so it can be reimplemented in any language\nin a few lines. The Node SDK's `verifyWebhookSignature` helper\nis the reference implementation; the wire details below let you\nwrite a verifier in Python, Go, Ruby, etc. without reading our\nsource.\n\n**Header**: `Primitive-Signature: t=<unix-seconds>,v1=<hex>`\n\nA legacy `MyMX-Signature` header is also sent on every delivery\nwith the same value, retained for back-compatibility with\nintegrations written before the rename. New code should read\n`Primitive-Signature`.\n\n**Signed string**: `${timestamp}.${rawBody}` where `timestamp`\nis the Unix-seconds integer from the `t=` parameter and\n`rawBody` is the exact bytes of the HTTP request body BEFORE\nany JSON decoding. Verify against the raw body, not a\nre-serialized parse, or you will silently mismatch on\ninsignificant whitespace.\n\n**Signature**: HMAC-SHA256 of the signed string, hex-encoded\n(lowercase). Use the account's webhook secret as the HMAC key,\nas a UTF-8 byte sequence.\n\n**Secret**: returned by `GET /account/webhook-secret`. The\nstring looks base64-shaped (e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`)\nbut is NOT base64; use it AS-IS as a UTF-8 string for the HMAC\nkey. Base64-decoding before HMAC will silently produce\nmismatched signatures.\n\n**Tolerance**: by convention, reject deliveries whose `t=`\ntimestamp is more than 5 minutes off your wall-clock to defend\nagainst replay attacks. The Node SDK's helper enforces this by\ndefault.\n\n**Verification recipe** (any language):\n\n```\n1. Read the raw HTTP body (do not parse).\n2. Read `Primitive-Signature: t=<ts>,v1=<sig>`.\n3. Reject if abs(now - ts) > 300 seconds.\n4. expected = HMAC_SHA256_hex(secret_utf8, f\"{ts}.{rawBody}\")\n5. Constant-time compare expected to sig. Reject if not equal.\n```\n\nFor Node, use `verifyWebhookSignature` from\n`@primitivedotdev/sdk/webhook` (or the higher-level\n`handleWebhook` helper if you want a one-liner). For other\nlanguages, the recipe above is everything you need.\n\nTest deliveries: `POST /endpoints/{id}/test` triggers a fake\ndelivery to your endpoint URL, signed with your real account\nsecret, so you can confirm verification end-to-end without\nneeding real inbound mail. The test response carries the exact\n`signature` header value sent on the wire so you can compare\nstrings directly.\n\n\n## Errors\n\nEvery error response is the same JSON envelope (`{ \"success\": false, \"error\": { \"code\", \"message\" } }`), served as `application/json` with HTTP status codes, following the RFC 7807 problem-details shape. The `error.code` is a stable machine-readable string and `error.message` is human-readable.\n\n## Authorization and roles\n\nAccess is governed by organization role-based access control. Every organization member holds one of three roles — `owner`, `admin`, or `member` — and a credential inherits a role. **API keys** always act at `member` level, regardless of the role of the user who created them, so an API key can never perform owner- or admin-only actions. **OAuth access tokens** act with the authorizing user's current organization role, resolved on each request. Every operation in this spec is part of the member-level surface, so any valid credential can call it. Organization administration that is not part of this API — billing and organization settings — requires an `owner` or `admin` and is performed in the dashboard. Fine-grained per-key scopes (e.g. a send-only or read-only key) are on the roadmap; today the role model is the unit of access control.\n\n## Versioning\n\nThe current stable API is **v1**. All endpoints are served under `/v1/` and are covered by a backward-compatibility guarantee: existing fields and status codes will not change without a deprecation notice.\n\nBreaking changes are announced at least 6 months in advance via changelog and email. Deprecated operations and fields are marked `x-deprecated: true` in the spec and carry a plain-English description of the replacement. The `v1` path prefix is guaranteed stable indefinitely; backward-compatible additions (new optional fields, new endpoints) may be made at any time without a version bump."
contact:
name: Primitive
url: https://primitive.dev
license:
name: Proprietary
url: https://primitive.dev/terms
x-stability-level: stable
x-deprecation-policy: 'Breaking changes are announced at least 6 months in advance. Deprecated fields carry x-deprecated: true. The current stable version is v1.'
servers:
- url: https://api.primitive.dev/v1
description: Canonical API host (PRIMITIVE_API_BASE_URL). Carries every public API operation.
tags:
- name: Agent
description: Agent signup and authentication
paths:
/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:
# --- truncated at 32 KB (47 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/primitive/refs/heads/main/openapi/primitive-agent-api-openapi.yml