Primitive Discovery API

Unauthenticated entry point that lists the API base URL, how to obtain credentials, and the operations callable without a token.

OpenAPI Specification

primitive-discovery-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: Primitive Account Discovery 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: Discovery
  description: Unauthenticated entry point that lists the API base URL, how to obtain credentials, and the operations callable without a token.
paths:
  /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_<key>`.'
      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 <token>`.'
                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
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: 'API key with `prim_` prefix or OAuth access token with `prim_oat_` prefix: `Authorization: Bearer <token>`. 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