Primitive CLI API

Browser-assisted CLI authentication

OpenAPI Specification

primitive-cli-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: Primitive Account CLI 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: CLI
  description: Browser-assisted CLI authentication
paths:
  /cli/login/start:
    post:
      operationId: startCliLogin
      summary: Start CLI browser login
      description: 'Starts a browser-assisted CLI login session. The response includes a

        device code for polling and a user code that the user approves in the

        browser. This endpoint does not require an API key.

        '
      tags:
      - CLI
      security: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                device_name:
                  type: string
                  minLength: 1
                  maxLength: 80
                  description: Human-readable device name shown during browser approval
                metadata:
                  type: object
                  additionalProperties: true
                  description: Optional client metadata stored with the login session; serialized JSON must be 2048 bytes or fewer
      responses:
        '201':
          description: CLI login session created
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always `no-store`
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        device_code:
                          type: string
                          description: Opaque code used by the CLI to poll for approval
                        user_code:
                          type: string
                          pattern: ^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$
                          description: Short code the user confirms in the browser
                        verification_uri:
                          type: string
                          description: Browser URL where the user approves the login
                        verification_uri_complete:
                          type: string
                          description: Browser URL with the user code prefilled
                        expires_in:
                          type: integer
                          description: Seconds until the login session expires
                        interval:
                          type: integer
                          description: Minimum seconds between poll requests
                      required:
                      - device_code
                      - user_code
                      - verification_uri
                      - verification_uri_complete
                      - expires_in
                      - interval
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid request parameters
        '429':
          $ref: '#/components/responses/RateLimited'
          description: Rate limit exceeded
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
        schema:
          type: string
          minLength: 1
          maxLength: 255
  /cli/login/poll:
    post:
      operationId: pollCliLogin
      summary: Poll CLI browser login
      description: 'Polls a CLI login session until the browser approval either succeeds,

        is denied, expires, or is polled too quickly. The OAuth token set is

        created only after approval and is returned exactly once.

        '
      tags:
      - CLI
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                device_code:
                  type: string
                  minLength: 1
              required:
              - device_code
      responses:
        '200':
          description: CLI login approved and OAuth token set created
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always `no-store`
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        api_key:
                          type: string
                          description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
                        key_id:
                          type: string
                          format: uuid
                          description: Legacy alias for oauth_grant_id
                        key_prefix:
                          type: string
                          description: Legacy display prefix derived from access_token
                        access_token:
                          type: string
                          description: OAuth access token for CLI API authentication
                        refresh_token:
                          type: string
                          description: OAuth refresh token used by the CLI to renew access
                        token_type:
                          type: string
                          enum:
                          - Bearer
                        expires_in:
                          type: integer
                          description: Seconds until access_token expires
                        auth_method:
                          type: string
                          enum:
                          - oauth
                        oauth_grant_id:
                          type: string
                          format: uuid
                        oauth_client_id:
                          type: string
                        org_id:
                          type: string
                          format: uuid
                        org_name:
                          type:
                          - string
                          - 'null'
                      required:
                      - api_key
                      - key_id
                      - key_prefix
                      - access_token
                      - refresh_token
                      - token_type
                      - expires_in
                      - auth_method
                      - oauth_grant_id
                      - oauth_client_id
                      - org_id
                      - org_name
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid request, pending authorization, slow polling, expired token, or invalid device code
        '403':
          $ref: '#/components/responses/Forbidden'
          description: CLI login was denied in the browser
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
        schema:
          type: string
          minLength: 1
          maxLength: 255
  /cli/signup/start:
    post:
      operationId: startCliSignup
      summary: Start CLI account signup
      description: 'Starts a terminal-native CLI signup. `signup_code` is optional;

        omit it to sign up without one. The API creates a pending signup

        session, sends an email verification code, and returns an opaque

        signup token used by the resend and verify steps. This endpoint

        does not require an API key.

        '
      tags:
      - CLI
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                signup_code:
                  type: string
                  minLength: 1
                  maxLength: 128
                  description: Optional signup code. Omit if you do not have one.
                terms_accepted:
                  type: boolean
                  const: true
                  description: Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy
                device_name:
                  type: string
                  minLength: 1
                  maxLength: 80
                  description: Human-readable device name used for the created CLI OAuth grant
                metadata:
                  type: object
                  additionalProperties: true
                  description: Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer
              required:
              - email
              - terms_accepted
      responses:
        '201':
          description: CLI signup session created and verification email sent
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always `no-store`
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        signup_token:
                          type: string
                          description: Opaque token used to verify or resend the pending CLI signup
                        email:
                          type: string
                          format: email
                        expires_in:
                          type: integer
                          description: Seconds until the pending signup expires
                        resend_after:
                          type: integer
                          description: Minimum seconds before requesting another verification email
                        verification_code_length:
                          type: integer
                          description: Number of digits in the emailed verification code
                      required:
                      - signup_token
                      - email
                      - expires_in
                      - resend_after
                      - verification_code_length
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid request parameters
        '429':
          $ref: '#/components/responses/RateLimited'
          description: Rate limit exceeded
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
        schema:
          type: string
          minLength: 1
          maxLength: 255
  /cli/signup/resend:
    post:
      operationId: resendCliSignupVerification
      summary: Resend CLI signup verification code
      description: 'Sends a new email verification code for a pending CLI signup session.

        This endpoint does not require an API key.

        '
      tags:
      - CLI
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                signup_token:
                  type: string
                  minLength: 1
              required:
              - signup_token
      responses:
        '200':
          description: Verification email resent
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always `no-store`
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        email:
                          type: string
                          format: email
                        expires_in:
                          type: integer
                          description: Seconds until the pending signup expires
                        resend_after:
                          type: integer
                          description: Minimum seconds before requesting another verification email
                        verification_code_length:
                          type: integer
                          description: Number of digits in the emailed verification code
                      required:
                      - email
                      - expires_in
                      - resend_after
                      - verification_code_length
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid token or expired token
        '429':
          $ref: '#/components/responses/RateLimited'
          description: Global rate limit exceeded or resend requested too quickly
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
        schema:
          type: string
          minLength: 1
          maxLength: 255
  /cli/signup/verify:
    post:
      operationId: verifyCliSignup
      summary: Verify CLI signup and create OAuth session
      description: 'Verifies the email code for a CLI signup session and creates the

        account. When the session was started with a `signup_code`, the

        reserved code is redeemed; sessions started without a code skip

        the redemption step. Either way an org-scoped OAuth CLI session

        is created and the token set is returned exactly once. This

        endpoint does not require an API key.

        '
      tags:
      - CLI
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                signup_token:
                  type: string
                  minLength: 1
                verification_code:
                  type: string
                  minLength: 1
                  maxLength: 32
                password:
                  type: string
                  minLength: 1
                  maxLength: 1024
              required:
              - signup_token
              - verification_code
      responses:
        '200':
          description: CLI signup verified and OAuth token set created
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always `no-store`
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        api_key:
                          type: string
                          description: Legacy alias for access_token. New CLI builds should persist access_token and refresh_token.
                        key_id:
                          type: string
                          format: uuid
                          description: Legacy alias for oauth_grant_id
                        key_prefix:
                          type: string
                          description: Legacy display prefix derived from access_token
                        access_token:
                          type: string
                          description: OAuth access token for CLI API authentication
                        refresh_token:
                          type: string
                          description: OAuth refresh token used by the CLI to renew access
                        token_type:
                          type: string
                          enum:
                          - Bearer
                        expires_in:
                          type: integer
                          description: Seconds until access_token expires
                        auth_method:
                          type: string
                          enum:
                          - oauth
                        oauth_grant_id:
                          type: string
                          format: uuid
                        oauth_client_id:
                          type: string
                        org_id:
                          type: string
                          format: uuid
                        org_name:
                          type:
                          - string
                          - 'null'
                      required:
                      - api_key
                      - key_id
                      - key_prefix
                      - access_token
                      - refresh_token
                      - token_type
                      - expires_in
                      - auth_method
                      - oauth_grant_id
                      - oauth_client_id
                      - org_id
                      - org_name
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid request, invalid verification code, expired token, invalid signup code, rejected password, or account creation failure
        '429':
          $ref: '#/components/responses/RateLimited'
          description: Rate limit exceeded
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
        schema:
          type: string
          minLength: 1
          maxLength: 255
  /cli/logout:
    post:
      operationId: cliLogout
      summary: Revoke the current CLI OAuth session
      description: 'Revokes the OAuth grant used to authenticate the request. API-key

        authenticated legacy logout requests succeed without deleting server API

        keys so old local CLI state can be cleared safely.

        '
      tags:
      - CLI
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                key_id:
                  type: string
                  format: uuid
                  description: Optional id guard; when provided it must match the authenticated OAuth grant id or API key id
      responses:
        '200':
          description: CLI logout completed
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        revoked:
                          type: boolean
                          description: True when an OAuth grant was revoked. False for API-key-authenticated legacy logout, which only clears local CLI state.
                        key_id:
                          type: string
                          format: uuid
                          description: API key id for API-key-authenticated legacy logout
                        oauth_grant_id:
                          type: string
                          format: uuid
                          description: OAuth grant id revoked by OAuth-authenticated logout
                      required:
                      - revoked
          headers:
            ratelimit-limit:
              description: Maximum number of requests allowed in the current window.
              schema:
                type: integer
                minimum: 1
                example: 120
            ratelimit-remaining:
              description: Remaining requests in the current window.
              schema:
                type: integer
                minimum: 0
                example: 118
            ratelimit-reset:
              description: Unix timestamp (seconds) when the current window resets.
              schema:
                type: integer
                example: 1700000060
            ratelimit-policy:
              description: Rate-limit policy in `limit;w=seconds` format, e.g. `120;w=60`.
              schema:
                type: string
                example: 120;w=60
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid request parameters
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: Invalid or missing API key
        '403':
          $ref: '#/components/responses/Forbidden'
          description: Authenticated caller lacks permission for the operation
        '404':
          $ref: '#/components/responses/NotFound'
          description: Resource not found
      security:
      - BearerAuth: []
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied idempotency key. Retrying a request with the same key returns the original result instead of performing the action a second time; if omitted the server derives one from the canonical payload hash. Safe to retry network failures without duplicating side effects.
        schema:
          type: string
          minLength: 1
          maxLength: 255
components:
  responses:
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: rate_limit_exceeded
              message: Rate limit exceeded
    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: unauthorized
              message: Invalid or missing API key
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: not_found
              message: Resource not found
    Forbidden:
      description: Authenticated caller lacks permission for the operation
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: forbidden
              message: Insufficient permissions
    ValidationError:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: validation_error
              message: Invalid domain format
  schemas:
    GateDenial:
      type: object
      properties:


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