groundmail-dev Tenants API

Your own customers inside one Groundmail account, each with its own domains, suppression list, stats, sending ramp and pause switch. Included on Scale and Dedicated.

Operations 9

GET /tenants List tenants #
POST /tenants Create a tenant #
GET /tenants/{id} Retrieve a tenant #
PATCH /tenants/{id} Update, pause or resume a tenant #
DELETE /tenants/{id} Delete a tenant #
GET /tenants/{id}/stats A tenant's email counts and sending ramp #
GET /tenants/{id}/domains List a tenant's domains #
POST /tenants/{id}/domains Give an existing domain to a tenant #
DELETE /tenants/{id}/domains/{domain} Give a tenant's domain back to the account #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/groundmail-dev:groundmail-dev-tenants-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

groundmail-dev-tenants-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Groundmail Tenants API
  version: 0.16.0
  description: 'Transactional email API for developers.

    Auth: `Authorization: Bearer gm_…` with an API key, or `Bearer gm_oat_…` with an OAuth

    access token from the device flow (`groundmail login`). Dashboard session cookies work

    for integrations.


    Azure managed identity (keyless): an Azure workload can send a Microsoft Entra access

    token (a JWT) as the Bearer credential instead of a key. Register the identity''s tenant

    ID, object ID and token audience in the dashboard (API keys → Azure managed identity);

    it then has the same access options as an API key (scopes, domain limit, tenant).

    App-only tokens only. Any authentication failure is the same 401 `unauthorized`.

    See https://docs.groundmail.dev/authentication/azure-managed-identity.


    Test mode: a test key (`gm_test_…`, `POST /api-keys` with `test: true`) behaves

    like a live key, but its sends are never delivered: they are stored with

    `test: true`, get simulated events and webhooks (`data.test: true`), and do not

    count toward plan limits, the sending ramp, stats or billing. Simulator

    addresses (`delivered@`, `bounce@`, `block@`, `complaint@`, `suppressed@`,

    `delayed@simulator.groundmail.dev`, plus addressing allowed) produce one outcome

    each with any key. See https://docs.groundmail.dev/testing.


    Tenants: a platform that sends email for its own customers creates one tenant per

    customer (`/tenants`), each with its own domains, suppression list, stats, sending

    ramp and pause switch. Name the tenant with `tenant` on sends, domains and

    suppressions, or send from a domain the tenant owns. API keys and webhook

    endpoints can be limited to tenants. Included on pay as you go and the Dedicated plan;

    elsewhere a request that names a tenant gets 403 `tenants_not_enabled`. See

    https://docs.groundmail.dev/tenants.


    Data retention: sent emails (content and events) and received emails are kept for

    your plan''s window, then deleted: 7 days on Free, 30 on Pro, 90 on Scale, up to 365

    on Dedicated. Past it, `GET /emails/{id}` and `GET /received-emails/{id}` return

    404 `not_found` and lists no longer include them. Aggregate stats

    (`GET /deliverability`) are kept. Store webhook events to keep history longer. See

    https://docs.groundmail.dev/emails#data-retention.


    Scopes: every credential carries scopes (`full`, `emails:send`, `emails:read`,

    `domains:read`, `domains:write`, `webhooks:write`, `templates:write`,

    `suppressions:write`, `account:read`). Each operation lists the scope it needs under

    `x-required-scope` (space-separated: all of them; `a | b`: either); a credential

    without it gets 403 `insufficient_scope` with `required_scope`. `full` grants everything; `domains:write` includes `domains:read`.

    Legacy `sending` API keys have `emails:send` + `emails:read`.

    Production base URL: https://api.groundmail.dev'
servers:
- url: https://api.groundmail.dev
  description: Production
tags:
- name: Tenants
  description: Your own customers inside one Groundmail account, each with its own domains, suppression list, stats, sending ramp and pause switch. Included on Scale and Dedicated.
paths:
  /tenants:
    get:
      operationId: listTenants
      x-required-scope: full
      summary: List tenants
      description: Tenants are your own customers inside one Groundmail account, each with its own domains, suppression list, stats, sending ramp and pause switch. Included on the Scale (up to 1,000 tenants) and Dedicated (up to 10,000) plans; other plans get 403 `tenants_not_enabled`.
      tags:
      - Tenants
      security:
      - bearerAuth: []
      parameters:
      - name: status
        in: query
        schema:
          type: string
          enum:
          - active
          - paused
        description: Only tenants with this status
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
        description: Maximum number of tenants to return (1–100)
      - name: cursor
        in: query
        schema:
          type: string
        description: Opaque cursor for the next page
      responses:
        '200':
          description: A page of tenants
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListTenantsResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Tenants are not included on this account's plan (`tenants_not_enabled`), or the key is not full access or is limited to one tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createTenant
      x-required-scope: full
      summary: Create a tenant
      tags:
      - Tenants
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTenantRequest'
            example:
              external_id: cust_8412
              name: Acme Inc
              default_from_domain: mail.acme.com
      responses:
        '201':
          description: Tenant created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          description: Invalid field
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: '`tenants_not_enabled`: tenants are not included on this account''s plan. `plan_limit`: the account has as many tenants as its plan includes (`details.tenant_limit`).

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A tenant with this `external_id` already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /tenants/{id}:
    parameters:
    - name: id
      in: path
      required: true
      description: The tenant's `external_id` or `id`
      schema:
        type: string
    get:
      operationId: getTenant
      x-required-scope: full
      summary: Retrieve a tenant
      description: A key limited to one tenant can read its own tenant.
      tags:
      - Tenants
      security:
      - bearerAuth: []
      responses:
        '200':
          description: The tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Tenants not enabled, or a tenant key asking for another tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateTenant
      x-required-scope: full
      summary: Update, pause or resume a tenant
      description: 'Change any setting from create (except `external_id`, which cannot change), or

        pause and resume the tenant with `status` and an optional `reason`. A paused

        tenant''s new sends get 403 `forbidden`; your other tenants keep sending.

        A tenant Groundmail paused (`pause_source` `automated` or `admin`) cannot be

        resumed here: fix the cause and contact support.'
      tags:
      - Tenants
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenantRequest'
            example:
              status: paused
              reason: Customer asked us to stop sending
      responses:
        '200':
          description: The updated tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          description: Invalid field
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Tenants not enabled, or resuming a tenant Groundmail paused
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: deleteTenant
      x-required-scope: full
      summary: Delete a tenant
      description: Deletes the tenant and its suppression list. Its domains go back to the account, its sent emails stay in your history, API keys limited to it are revoked, and it is removed from webhook tenant filters (an endpoint left with no tenants is disabled).
      tags:
      - Tenants
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Tenant deleted
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - id
                - external_id
                - deleted
                properties:
                  ok:
                    type: boolean
                  id:
                    type: string
                  external_id:
                    type: string
                  deleted:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /tenants/{id}/stats:
    parameters:
    - name: id
      in: path
      required: true
      description: The tenant's `external_id` or `id`
      schema:
        type: string
    get:
      operationId: getTenantStats
      x-required-scope: full
      summary: A tenant's email counts and sending ramp
      description: Counts and rates for the tenant's emails created in the last `days`, and its sending ramp today. A key limited to one tenant can read its own tenant's stats.
      tags:
      - Tenants
      security:
      - bearerAuth: []
      parameters:
      - name: days
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 90
          default: 7
        description: Window in days (1–90)
      responses:
        '200':
          description: The tenant's stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantStatsResponse'
        '400':
          description: '`days` out of range'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /tenants/{id}/domains:
    parameters:
    - name: id
      in: path
      required: true
      description: The tenant's `external_id` or `id`
      schema:
        type: string
    get:
      operationId: listTenantDomains
      x-required-scope: full
      summary: List a tenant's domains
      description: A key limited to one tenant can list its own tenant's domains.
      tags:
      - Tenants
      security:
      - bearerAuth: []
      responses:
        '200':
          description: The tenant's domains, each with `tenant`
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Domain'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: assignTenantDomain
      x-required-scope: full
      summary: Give an existing domain to a tenant
      description: Only this tenant can then send from the domain. A send from it without `tenant` is attributed to this tenant. To add a new domain straight to a tenant, use `POST /domains` with `tenant`.
      tags:
      - Tenants
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - domain
              properties:
                domain:
                  type: string
                  description: Domain name or id
            example:
              domain: mail.acme.com
      responses:
        '200':
          description: The domain, now with `tenant`
          content:
            application/json:
              schema:
                type: object
                required:
                - domain
                properties:
                  domain:
                    $ref: '#/components/schemas/Domain'
        '400':
          description: '`domain` missing'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No such tenant, or no such domain in this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Another tenant owns the domain. Remove it from that tenant first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /tenants/{id}/domains/{domain}:
    parameters:
    - name: id
      in: path
      required: true
      description: The tenant's `external_id` or `id`
      schema:
        type: string
    - name: domain
      in: path
      required: true
      description: Domain name or id
      schema:
        type: string
    delete:
      operationId: removeTenantDomain
      x-required-scope: full
      summary: Give a tenant's domain back to the account
      description: The domain itself stays; every tenant can send from it again.
      tags:
      - Tenants
      security:
      - bearerAuth: []
      responses:
        '200':
          description: The domain, now account-wide
          content:
            application/json:
              schema:
                type: object
                required:
                - domain
                properties:
                  domain:
                    $ref: '#/components/schemas/Domain'
        '404':
          description: The tenant has no such domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    TenantStats:
      type: object
      properties:
        object:
          type: string
          const: tenant_stats
        days:
          type: integer
        from:
          type: string
          format: date-time
        total:
          type: integer
        queued:
          type: integer
        scheduled:
          type: integer
        sent:
          type: integer
        delivered:
          type: integer
        bounced:
          type: integer
        complained:
          type: integer
        failed:
          type: integer
        suppressed:
          type: integer
        opened:
          type: integer
        clicked:
          type: integer
        bounce_rate:
          type: number
        complaint_rate:
          type: number
    Tenant:
      type: object
      required:
      - id
      - object
      - external_id
      - name
      - status
      - stream_policy
      - created_at
      - updated_at
      properties:
        id:
          type: string
          format: uuid
        object:
          type: string
          const: tenant
        external_id:
          type: string
        name:
          type: string
        status:
          type: string
          enum:
          - active
          - paused
        paused_reason:
          type:
          - string
          - 'null'
        paused_at:
          type:
          - string
          - 'null'
          format: date-time
        pause_source:
          type:
          - string
          - 'null'
          enum:
          - api
          - admin
          - automated
          - null
          description: 'Who paused the tenant: `api` (you), `automated` (Groundmail, after a high bounce or complaint rate) or `admin` (Groundmail support).

            '
        default_from_domain:
          type:
          - string
          - 'null'
        stream_policy:
          type: string
          enum:
          - all
          - transactional_only
        daily_limit:
          type:
          - integer
          - 'null'
        metadata:
          type:
          - object
          - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateTenantRequest:
      allOf:
      - type: object
        required:
        - external_id
        properties:
          external_id:
            type: string
            pattern: ^[A-Za-z0-9._:@-]{1,128}$
            description: Your id for this customer. Unique in your account; cannot change.
      - $ref: '#/components/schemas/TenantSettings'
    ErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - message
          properties:
            message:
              type: string
              description: What went wrong, in plain English. Wording can change; branch on `code`.
            code:
              $ref: '#/components/schemas/ErrorCode'
            hint:
              type: string
              description: 'How to fix the error. Specific to the failure when known (for example a sandbox recipient), otherwise the default for `code`.

                '
            docs_url:
              type: string
              format: uri
              description: Link to this code on the errors page, `https://docs.groundmail.dev/errors#<code>`.
              example: https://docs.groundmail.dev/errors#validation_error
            upgrade_url:
              type: string
              description: 'Dashboard path to upgrade when code is plan_limit or domain_limit. Absent for `daily_ramp_limit`, which upgrading does not lift.

                '
            required_scope:
              type: string
              description: 'With `insufficient_scope`: the scope (or space-separated scopes) the operation needs. Also sent as `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"`.

                '
            details:
              description: 'Optional structured details. For send blocks this may include `suppressed` (global suppression list) or `category_unsubscribed` plus `category` and `email_id` (category opt-out). A `daily_ramp_limit` carries `stream` (`transactional` or `product`), `daily_limit` (today''s recipient limit for that stream), `used_today`, `recipients` (in this send), `ramp_stage` and `resets_at` (next 00:00 UTC, ISO 8601), plus `tenant` when the tenant''s own limit refused the send.

                '
    ListTenantsResponse:
      type: object
      required:
      - data
      - has_more
      - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Tenant'
        has_more:
          type: boolean
        next_cursor:
          type:
          - string
          - 'null'
    UpdateTenantRequest:
      allOf:
      - $ref: '#/components/schemas/TenantSettings'
      - type: object
        properties:
          status:
            type: string
            enum:
            - active
            - paused
            description: '`paused` stops the tenant''s new sends; `active` resumes them.'
          reason:
            type:
            - string
            - 'null'
            description: Why you paused the tenant.
    TenantRamp:
      type: object
      description: 'The tenant''s sending ramp today. Each new tenant starts at 1,000 product and 3,000 transactional recipients per UTC day, and the limit doubles each day it used at least half of it with bounces under 2% and complaints under 0.1%.

        '
      required:
      - object
      - active
      - stage
      - daily_limit
      - used_today
      - completed
      - resets_at
      properties:
        object:
          type: string
          const: tenant_ramp
        active:
          type: boolean
          description: '`false` when no ramp limit applies: the tenant has not sent yet, its ramp is complete, or support lifted it.'
        stage:
          type:
          - integer
          - 'null'
          description: Current stage while the ramp is active.
        daily_limit:
          type:
          - object
          - 'null'
          description: Today's recipient limits per stream; `null` when the ramp is not active.
          properties:
            transactional:
              type:
              - integer
              - 'null'
            product:
              type:
              - integer
              - 'null'
        used_today:
          type: object
          properties:
            transactional:
              type: integer
            product:
              type: integer
        completed:
          type: boolean
        resets_at:
          type: string
          format: date-time
          description: The next 00:00 UTC.
    Domain:
      type: object
      required:
      - id
      - name
      - status
      - created_at
      - verified_at
      - open_tracking
      - click_tracking
      - tracking_status
      - tracking_error
      - tls_mode
      - inbound_enabled
      - inbound_status
      - inbound_error
      properties:
        id:
          type: string
        name:
          type: string
          example: example.com
        status:
          type: string
          enum:
          - pending
          - verified
          - failed
        created_at:
          type: string
          format: date-time
        verified_at:
          type: string
          format: date-time
          nullable: true
        open_tracking:
          type: boolean
          description: Open tracking preference for this domain.
        click_tracking:
          type: boolean
          description: Click tracking preference for this domain.
        tracking_status:
          type: string
          enum:
          - disabled
          - pending
          - active
          - error
          description: Whether open/click tracking is applied to outgoing mail. `disabled` when both preferences are off, `pending` until the tracking CNAME is accepted and a certificate is issued for the tracking hostname (links and opens are not tracked yet; this takes a few minutes after the CNAME is added), `active` once both are done, `error` when tracking or the certificate could not be set up. Re-checked when you verify the domain and when you read it.
        tracking_error:
          type: string
          nullable: true
          description: Why tracking is not active yet, when `tracking_status` is `pending` or `error`.
        tls_mode:
          type: string
          enum:
          - opportunistic
          - enforced
          description: Preferred outbound TLS behavior for this domain.
        inbound_enabled:
          type: boolean
          description: True when Groundmail should accept inbound mail for this domain.
        inbound_status:
          type: string
          enum:
          - disabled
          - pending
          - active
          - error
          description: '`pending` before verification, `active` once inbound receiving is configured, or `error` when setup fails.'
        inbound_error:
          type: string
          nullable: true
          description: Last inbound configuration error, if any.
        inbound_spam_threshold:
          type: number
          nullable: true
          description: Received mail scoring at or above this is `spam` (and dropped when `inbound_spam_action` is `drop`). Null uses the default (10) and drops nothing.
        inbound_spam_action:
          type: string
          enum:
          - flag
          - drop
          description: '`flag` keeps spam with `spam_status: spam`; `drop` discards it (no stored email, no webhook).'
        tenant:
          type: string
          description: The `external_id` of the tenant that owns this domain. Present only on a tenant's domain.
    TenantResponse:
      type: object
      required:
      - tenant
      properties:
        tenant:
          $ref: '#/components/schemas/Tenant'
    TenantSettings:
      type: object
      properties:
        name:
          type: string
          maxLength: 200
          description: Display name. Defaults to `external_id`.
        default_from_domain:
          type:
          - string
          - 'null'
          description: Completes a bare `from` local part (`"billing"` → `billing@<domain>`) on sends that name this tenant.
        stream_policy:
          type: string
          enum:
          - all
          - transactional_only
          default: all
          description: '`transactional_only` refuses product email for this tenant.'
        daily_limit:
          type:
          - integer
          - 'null'
          minimum: 0
          description: 'Most recipients this tenant may email per UTC day, on top of its sending ramp. `null` for no limit of your own.

            '
        metadata:
          type:
          - object
          - 'null'
          description: Any JSON object, up to 4 KB, for your own use.
    TenantStatsResponse:
      type: object
      required:
      - tenant
      - stats
      properties:
        tenant:
          type: string
          description: The tenant's `external_id`.
        stats:
          $ref: '#/components/schemas/TenantStats'
        ramp:
          $ref: '#/components/schemas/TenantRamp'
    ErrorCode:
      type: string
      description: 'Stable, machine-readable error code. Each code is documented at

        `https://docs.groundmail.dev/errors#<code>`. New codes may be added;

        existing codes keep their meaning.


        - `validation_error` (400, 422): the request failed validation, or every recipient is suppressed or opted out (422). Fix the field named in `message`/`details`.

        - `unauthorized` (401): missing, malformed or revoked API key. Send `Authorization: Bearer gm_…`.

        - `forbidden` (403): the key is valid but the action is not allowed (unverified `from` domain, sandbox send to someone other than the account email, no payment method on file, sending restricted).

        - `insufficient_scope` (403): the API key''s scope does not cover this endpoint.

        - `domain_not_allowed` (403): the API key is limited to other sending domains than the one in `from`, or `from` is on another tenant''s domain.

        - `tenants_not_enabled` (403): the request names a tenant, but tenants are not enabled for this account. They are included on pay as you go and the Dedicated plan.

        - `newsletter_not_enabled` (403): the send uses `category: "newsletter"`, but newsletter sending isn''t enabled on this account. Newsletter email is accepted only for approved accounts.

        - `not_found` (404): no resource with that id for this team.

        - `conflict` (409): the resource already exists or its state blocks the change.

        - `plan_limit` (429): the plan''s monthly or daily sending limit was reached. See `upgrade_url`.

        - `daily_ramp_limit` (429): the send would go over a new account''s daily ramp-up limit for its stream, or its tenant''s own ramp or daily limit (`details.tenant`). Upgrading does not lift it; wait for `details.resets_at`.

        - `domain_limit` (403): the plan''s domain limit was reached. See `upgrade_url`.

        - `idempotency_conflict` (409): the `Idempotency-Key` was reused with a different body, or the first request is still running.

        - `rate_limited` (429): too many requests. Wait `Retry-After` seconds.

        - `timeout` (408): a long-poll (GET /test-inbox/wait) ended before a matching email arrived. Safe to call again.

        - `upstream_error` (502, 503, 504, or a passed-through 4xx): the mail server did not accept the request. 502/503 are safe to retry with the same `Idempotency-Key`; 504 means the outcome is unknown.

        - `not_configured` (503): the feature is temporarily unavailable.

        - `internal_error` (500): unexpected server error. Retry with backoff.

        '
      enum:
      - validation_error
      - unauthorized
      - forbidden
      - insufficient_scope
      - not_found
      - conflict
      - plan_limit
      - daily_ramp_limit
      - domain_limit
      - idempotency_conflict
      - rate_limited
      - timeout
      - upstream_error
      - not_configured
      - internal_error
      - domain_not_allowed
      - tenants_not_enabled
      - newsletter_not_enabled
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Groundmail API key (`gm_…`), OAuth access token (`gm_oat_…`), or a Microsoft Entra access token from an Azure managed identity registered in the dashboard (keyless; see https://docs.groundmail.dev/authentication/azure-managed-identity). All carry scopes; see the API description.

        '