Primitive Endpoints API
Manage webhook endpoints that receive email events
Manage webhook endpoints that receive email events
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/primitive-endpoints-api"
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
openapi: 3.2.0
info:
title: Primitive Account Endpoints 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: Endpoints
description: Manage webhook endpoints that receive email events
paths:
/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
components:
responses:
RateLimited:
description: Rate l
# --- truncated at 32 KB (39 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/primitive/refs/heads/main/openapi/primitive-endpoints-api-openapi.yml