Primitive Functions API

Deploy JavaScript handlers that run on inbound mail. Each function is a single ESM module whose default export is an object with an async `fetch(request, env)` method, in the shape of a Workers-style handler. Primitive signs each delivery and forwards the `Primitive-Signature` header to the handler; verify the raw request body with `PRIMITIVE_WEBHOOK_SECRET` before trusting the parsed event. The `event` field is `email.received` for normal inbound mail, or a machine-mail type (`email.bounced`, `email.tls_report`, `email.dmarc_report`, `email.dmarc_failure`) for bounces and reports; the payload shape is otherwise identical. Code runs on Primitive's edge runtime; there is no infrastructure to manage. Secrets land in `env` as encrypted bindings and are refreshed on every redeploy.

OpenAPI Specification

primitive-functions-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: Primitive Account Functions 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: Functions
  description: 'Deploy JavaScript handlers that run on inbound mail. Each function

    is a single ESM module whose default export is an object with an

    async `fetch(request, env)` method, in the shape of a Workers-style

    handler. Primitive signs each delivery and forwards the

    `Primitive-Signature` header to the handler; verify the raw request

    body with `PRIMITIVE_WEBHOOK_SECRET` before trusting the parsed event.

    The `event` field is `email.received` for normal inbound mail, or a

    machine-mail type (`email.bounced`, `email.tls_report`,

    `email.dmarc_report`, `email.dmarc_failure`) for bounces and reports;

    the payload shape is otherwise identical. Code runs on

    Primitive''s edge runtime; there is no infrastructure to manage.

    Secrets land in `env` as encrypted bindings and are refreshed on

    every redeploy.

    '
paths:
  /functions:
    get:
      operationId: listFunctions
      summary: List functions
      description: 'Returns every active (non-deleted) function in the org, newest

        first. Each entry carries deploy status and timestamps. To

        inspect the source code or deploy errors, use `GET /functions/{id}`.

        '
      tags:
      - Functions
      responses:
        '200':
          description: List of functions
          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
                        description: One row from the function listing.
                        properties:
                          id:
                            type: string
                            format: uuid
                            description: Function id, also the script name in the edge runtime.
                          name:
                            type: string
                            description: Slug-style name set on creation. Stable; cannot be changed.
                          deploy_status:
                            type: string
                            enum:
                            - pending
                            - deployed
                            - failed
                            description: "Lifecycle state of the latest deploy attempt:\n  * `pending` — deploy in flight; the runtime has not yet\n    confirmed the new bundle is live.\n  * `deployed` — the running edge handler is the latest code.\n  * `failed` — the most recent deploy attempt failed; the\n    previously-live code (if any) is still running. The\n    `deploy_error` field carries the error message.\n"
                          deployed_at:
                            type:
                            - string
                            - 'null'
                            format: date-time
                            description: Timestamp of the most recent successful deploy. Null until the first deploy succeeds.
                          created_at:
                            type: string
                            format: date-time
                          updated_at:
                            type: string
                            format: date-time
                        required:
                        - id
                        - name
                        - deploy_status
                        - created_at
                        - updated_at
          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: createFunction
      summary: Deploy a function
      description: 'Creates and deploys a new function. The handler must be a single

        ESM module whose default export is an object with an async

        `fetch(request, env)` method (Workers-style). Primitive signs

        each delivery and forwards the `Primitive-Signature` header to

        the handler. Verify the raw request body with

        `PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification

        the request body parses to a webhook event whose `event` field is

        `email.received` for normal inbound mail, or a machine-mail type

        (`email.bounced`, `email.tls_report`, `email.dmarc_report`,

        `email.dmarc_failure`) for bounces and reports. Code is bundled

        before being uploaded; ship a single self-contained file rather

        than relying on external imports.


        **Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`

        (optional) is capped at 5 MiB UTF-8, stored with each deployment

        attempt, and sent to the runtime so stack traces can resolve to

        original source files.


        **Routing.** On successful deploy, the function code is live

        in the runtime, but inbound mail will not reach it until at

        least one route is bound. Routes are managed from the Primitive

        dashboard. A `deploy_status` of `deployed` means the script is

        installed, not that the function is receiving mail. The

        internal runtime URL is not returned by the API and is not a

        customer-facing integration surface.


        **Secrets.** New functions ship with the managed secrets

        (`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,

        `PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via

        `POST /functions/{id}/secrets`; secret writes only land in the

        running handler on the next redeploy.

        '
      tags:
      - Functions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  pattern: ^[a-z0-9_-]{1,64}$
                  description: 'Slug-style name. Lowercase letters, digits, hyphens, and

                    underscores. 1 to 64 characters. Must be unique within the

                    org; a 409 is returned on collision.

                    '
                code:
                  type: string
                  minLength: 1
                  maxLength: 1048576
                  description: 'Pre-built handler as a single ESM module. Up to 1 MiB UTF-8.

                    Must export a default `{ async fetch(req, env, ctx) { ... } }`

                    object. Provide either `code` or `files`, not both.

                    '
                sourceMap:
                  type: string
                  minLength: 1
                  maxLength: 5242880
                  description: 'Optional source map for the bundle. Up to 5 MiB UTF-8.

                    Stored with the deployment attempt and sent to the runtime

                    to symbolicate stack traces in the function''s logs. Only

                    valid with `code`.

                    '
                files:
                  type: object
                  additionalProperties:
                    type: string
                  description: 'Source files for a managed build, as a map of path to file

                    contents (for example {"package.json": "...",

                    "src/index.ts": "..."}). Provide this INSTEAD of `code` to

                    have the server install dependencies and bundle the source

                    for the Workers runtime before deploying. Include a

                    package.json (its `dependencies` are installed). Provide

                    either `code` or `files`, not both.

                    '
              required:
              - name
      responses:
        '201':
          description: Function created and deployed
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      description: Returned by POST /functions on a successful deploy.
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        deploy_status:
                          type: string
                          enum:
                          - pending
                          - deployed
                          - failed
                          description: "Lifecycle state of the latest deploy attempt:\n  * `pending` — deploy in flight; the runtime has not yet\n    confirmed the new bundle is live.\n  * `deployed` — the running edge handler is the latest code.\n  * `failed` — the most recent deploy attempt failed; the\n    previously-live code (if any) is still running. The\n    `deploy_error` field carries the error message.\n"
                      required:
                      - id
                      - name
                      - deploy_status
        '400':
          $ref: '#/components/responses/ValidationError'
          description: Invalid request parameters or customer-correctable deploy rejection
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: Invalid or missing API key
        '409':
          $ref: '#/components/responses/Conflict'
          description: A function with this name already exists in the org
        '424':
          $ref: '#/components/responses/FailedDependency'
          description: Function deploy could not be completed; previously deployed code remains live
        '429':
          $ref: '#/components/responses/RateLimited'
          description: Function deploy could not be completed; previously deployed code remains live
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: Function deploy could not be completed; previously deployed code remains live
      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
  /functions/{id}:
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Resource UUID
    get:
      operationId: getFunction
      summary: Get a function
      description: 'Returns the full record for a function, including its current

        source code and the deploy status / error from the most recent

        deploy attempt.

        '
      tags:
      - Functions
      responses:
        '200':
          description: Function record
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      description: Full function record returned by GET / PUT.
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        code:
                          type: string
                          description: 'The bundled handler source. UTF-8 string up to 1 MiB. The

                            same value most recently passed as `code` to POST or PUT.

                            '
                        deploy_status:
                          type: string
                          enum:
                          - pending
                          - deployed
                          - failed
                          description: "Lifecycle state of the latest deploy attempt:\n  * `pending` — deploy in flight; the runtime has not yet\n    confirmed the new bundle is live.\n  * `deployed` — the running edge handler is the latest code.\n  * `failed` — the most recent deploy attempt failed; the\n    previously-live code (if any) is still running. The\n    `deploy_error` field carries the error message.\n"
                        deploy_error:
                          type:
                          - string
                          - 'null'
                          description: 'Error message from the most recent failed deploy, or null

                            after a successful deploy. Surface this to users to explain

                            a `failed` status without polling.

                            '
                        deployed_at:
                          type:
                          - string
                          - 'null'
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                      required:
                      - id
                      - name
                      - code
                      - deploy_status
                      - created_at
                      - updated_at
          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: []
    put:
      operationId: updateFunction
      summary: Update and redeploy a function
      description: 'Replaces the function''s source code with the body''s `code` and

        triggers a redeploy. Same size limits as `POST /functions`.

        Use this verb to push secret writes into the running handler:

        passing the same `code` re-runs the deploy and refreshes the

        binding set with the latest values from the secrets table.


        On deploy failure, the previously-deployed code stays live; the

        runtime never serves a half-built bundle. The response uses

        `error.code` `deploy_failed`, and the function''s `deploy_error`

        field carries the latest deploy error for dashboard/API reads.

        '
      tags:
      - Functions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                code:
                  type: string
                  minLength: 1
                  maxLength: 1048576
                  description: New pre-built handler. Same rules as CreateFunctionInput.code. Provide either `code` or `files`, not both.
                sourceMap:
                  type: string
                  minLength: 1
                  maxLength: 5242880
                files:
                  type: object
                  additionalProperties:
                    type: string
                  description: 'Source files for a managed build, as a map of path to file

                    contents. Provide this INSTEAD of `code` to rebuild and

                    redeploy from source. Same rules as CreateFunctionInput.files.

                    '
              required: []
      responses:
        '200':
          description: Updated function
          content:
            application/json:
              schema:
                allOf:
                - type: object
                  properties:
                    success:
                      type: boolean
                      const: true
                  required:
                  - success
                  - data
                - type: object
                  properties:
                    data:
                      type: object
                      description: Full function record returned by GET / PUT.
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        code:
                          type: string
                          description: 'The bundled handler source. UTF-8 string up to 1 MiB. The

                            same value most recently passed as `code` to POST or PUT.

                            '
                        deploy_status:
                          type: string
                          enum:
                          - pending
                          - deployed
                          - failed
                          description: "Lifecycle state of the latest deploy attempt:\n  * `pending` — deploy in flight; the runtime has not yet\n    confirmed the new bundle is live.\n  * `deployed` — the running edge handler is the latest code.\n  * `failed` — the most recent deploy attempt failed; the\n    previously-live code (if any) is still running. The\n    `deploy_error` field carries the error message.\n"
                        deploy_error:
                          type:
                          - string
                          - 'null'
                          description: 'Error message from the most recent failed deploy, or null

                            after a successful deploy. Surface this to users to explain

                            a `failed` status without polling.

                            '
                        deployed_at:
                          type:
                          - string
                          - 'null'
                          format: date-time
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                      required:
                      - id
                      - name
                      - code
                      - deploy_status
                      - created_at
                      - updated_at
          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 or customer-correctable deploy rejection
        '401':
          $ref: '#/components/responses/Unauthorized'
          description: Invalid or missing API key
        '404':
          $ref: '#/components/responses/NotFound'
          description: Resource not found
        '424':
          $ref: '#/components/responses/FailedDependency'
          description: Function deploy could not be completed; previously deployed code remains live
        '429':
          $ref: '#/components/responses/RateLimited'
          description: Function deploy could not be completed; previously deployed code remains live
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: Function deploy could not be completed; previously deployed code remains live
      security:
      - BearerAuth: []
    delete:
      operationId: deleteFunction
      summary: Delete a function
      description: Delete a hosted inbound function. It stops running on future inbound messages.
      tags:
      - Functions
      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
        '502':
          $ref: '#/components/responses/BadGateway'
          description: Primitive could not complete the downstream SMTP request
      security:
      - BearerAuth: []
  /functions/{id}/test:
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Resource UUID
    post:
      operationId: testFunction
      summary: Send a test invocation
      description: 'Sends a real test email from a Primitive-controlled sender to a

        local-part on one of the org''s verified inbound domains. By

        default the recipient is a synthetic

        `__primitive_function_test+<random>@<domain>` address on a

        domain selected to route to the function. Scoped functions use

        their scoped domain; fallback functions use a domain that has

        no enabled domain-scoped endpoint. Pass `local_part` to

        override and exercise routing logic that branches on a specific

        recipient (the common pattern when one function handles multiple

        inboxes like `summarize@` and `action@`). The function fires

        through the normal MX delivery path, so reply / send-mail calls

        from inside the handler against the inbound''s `email.id` work

        the same as in production. Returns immediately after the send is

        queued; the invocation appears on the function''s invocations

        list within a few seconds.


        Requires that the functio

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