Primitive Registries API

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.

OpenAPI Specification

primitive-registries-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: Primitive Account Registries 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: 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.

    '
paths:
  /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
  

# --- truncated at 32 KB (55 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/primitive/refs/heads/main/openapi/primitive-registries-api-openapi.yml