Anjal Drafts API

Compose messages across multiple agent turns and send atomically.

Operations 6

POST /v1/inboxes/{id}/drafts Create a draft #
GET /v1/inboxes/{id}/drafts List drafts #
GET /v1/inboxes/{id}/drafts/{did} Get a draft #
PATCH /v1/inboxes/{id}/drafts/{did} Update a draft #
DELETE /v1/inboxes/{id}/drafts/{did} Delete a draft #
POST /v1/inboxes/{id}/drafts/{did}/send Send a draft #

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/anjal-app:anjal-app-drafts-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

anjal-app-drafts-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Anjal Drafts API
  version: 1.0.0
  description: 'Anjal is a multi-tenant email API for AI agents.


    Create inboxes on the default `@send.anjal.app` domain or your own custom

    domains, send and receive mail, manage drafts, subscribe to real-time

    events over WebSocket, and isolate resources across workspaces so your

    reseller customers can serve their own end-customers under a single

    billing relationship.


    Install the TypeScript SDK: `npm install anjal`

    (npm)


    MCP server for Claude Desktop / Cursor: `npx -y anjal-mcp`

    (npm)


    ## Authentication


    Every request is authenticated with the `X-API-Key` header (or

    `Authorization: Bearer `). Four key prefixes, each with increasing

    scope:


    | Prefix | Scope | Use it for |

    |----------|-----------------------------------------|-----------------------------------------|

    | `sa_...` | Super admin | Tenant lifecycle (platform operator) |

    | `tk_...` | Tenant master | Everything in your tenant across workspaces |

    | `wk_...` | Workspace master | One workspace only |

    | `ak_...` | Inbox (agent) scoped | One inbox — its messages, drafts, lists |


    ## Resource hierarchy


    ```

    Tenant (billing root)

    └── Workspace (isolation container)

    ├── Inboxes

    │ ├── Messages / Threads / Attachments

    │ ├── Drafts

    │ └── Lists

    └── Domains

    ```


    ## Sending mail


    Mail is sent via `POST /v1/inboxes/{id}/send`. Replies go through

    `/reply/{messageId}`, reply-all through `/reply-all/{messageId}`, and

    forwards through `/forward/{messageId}`. The `From` header always uses

    the inbox''s **primary alias** — add a custom-domain address with

    `POST /v1/inboxes/{id}/addresses` to change it.


    ### Safe retries (Idempotency-Key)


    Every send endpoint (`/send`, `/reply`, `/reply-all`, `/forward` and

    `POST /v1/inboxes/{id}/drafts/{did}/send`) accepts an `Idempotency-Key`

    header. Send a unique value (a UUID works) and retry freely: the first

    request sends and stores its response; a retry with the same key and the

    identical request returns that stored response with the header

    `Idempotent-Replayed: true` and sends nothing.


    | Situation | Result |

    |---|---|

    | Same key, different request (body, inbox or endpoint) | `409 IDEMPOTENCY_KEY_REUSED` |

    | Same key while the first request is still running | `409 IDEMPOTENCY_IN_PROGRESS` |

    | Header present but blank, or longer than 255 characters | `400 IDEMPOTENCY_KEY_INVALID` |

    | The first send failed (non-2xx) | The key is freed; retry with the same key |


    Keys are scoped to your account and expire after 24 hours.


    ### Labels, open tracking, scheduled send


    - **Labels**: pass `labels` (array, or a comma-separated string) on any

    send, reply, reply-all, forward or draft. Change them later with

    `PATCH /v1/inboxes/{id}/messages/{messageId}` or

    `PATCH /v1/inboxes/{id}/threads/{threadId}`, and filter lists with

    `?labels=a,b`. Labels are stored lowercase and deduplicated; at most 32

    per message, each at most 64 characters, no commas.

    - **Open tracking**: `trackOpens: true` on a send with an HTML body adds a

    1x1 pixel (the stored copy never contains it). The first open fires

    `message.opened`. Opens are a hint, not proof: Apple Mail Privacy

    Protection fetches images on delivery, and clients that block images

    never report an open.

    - **Scheduled send**: set `sendAt` on a draft and Anjal sends it at that

    time. See the Drafts endpoints.

    - **Delivery status**: sent messages carry `deliveryStatus`

    (`delivered`, `bounced`, `complained` or `rejected`; null until known)

    and `deliveredAt`.


    ## Receiving mail


    Inbound mail arrives via SMTP on the default domain or any verified

    custom domain. Subscribe to delivery events either by registering a

    webhook (`POST /v1/webhooks`) or by opening a WebSocket

    (`wss://anjal.app/v1/ws`) — useful for in-line 2FA flows where

    the agent doesn''t have a public webhook receiver.'
servers:
- url: https://anjal.app
  description: Production
security:
- apiKeyHeader: []
tags:
- name: Drafts
  description: Compose messages across multiple agent turns and send atomically.
paths:
  /v1/inboxes/{id}/drafts:
    parameters:
    - $ref: '#/components/parameters/InboxId'
    post:
      tags:
      - Drafts
      summary: Create a draft
      description: '`labels` are applied to the message when the draft is sent. Set

        `sendAt` to schedule the send: Anjal sends it at that time, no cron

        needed.'
      operationId: createDraft
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftInput'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Draft'
        '400':
          description: Validation error, e.g. sendAt in the past, more than 90 days ahead, or a scheduled draft without recipients or a body
    get:
      tags:
      - Drafts
      summary: List drafts
      description: '`scheduled=true` lists only drafts waiting to be sent, soonest first. A failed scheduled send shows `sendError`.'
      operationId: listDrafts
      parameters:
      - $ref: '#/components/parameters/LimitQuery'
      - $ref: '#/components/parameters/OffsetQuery'
      - name: scheduled
        in: query
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  drafts:
                    type: array
                    items:
                      $ref: '#/components/schemas/Draft'
  /v1/inboxes/{id}/drafts/{did}:
    parameters:
    - $ref: '#/components/parameters/InboxId'
    - $ref: '#/components/parameters/DraftId'
    get:
      tags:
      - Drafts
      summary: Get a draft
      operationId: getDraft
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Draft'
    patch:
      tags:
      - Drafts
      summary: Update a draft
      description: 'Merge-patch. Omitted fields are left as-is. Set `sendAt` to schedule or

        reschedule the send (this also clears `sendError`); `sendAt: null`

        cancels it.'
      operationId: updateDraft
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftInput'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Draft'
                - type: object
                  properties:
                    updated:
                      type: boolean
        '400':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags:
      - Drafts
      summary: Delete a draft
      operationId: deleteDraft
      responses:
        '200':
          description: Deleted
  /v1/inboxes/{id}/drafts/{did}/send:
    parameters:
    - $ref: '#/components/parameters/InboxId'
    - $ref: '#/components/parameters/DraftId'
    post:
      tags:
      - Drafts
      summary: Send a draft
      description: Sends the current draft contents now and deletes the draft atomically. If the draft has `replyToMessageId`, the sent message is threaded as a reply. The draft's `labels` are applied to the sent message. Accepts an `Idempotency-Key` header.
      operationId: sendDraft
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  threadId:
                    type: string
                    format: uuid
                  status:
                    type: string
                  messageId:
                    type: string
                  draftId:
                    type: string
                    format: uuid
        '400':
          description: Validation error, or IDEMPOTENCY_KEY_INVALID
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
components:
  schemas:
    DraftInput:
      type: object
      properties:
        to:
          oneOf:
          - type: string
            format: email
          - type: array
            items:
              type: string
              format: email
        cc:
          oneOf:
          - type: string
            format: email
          - type: array
            items:
              type: string
              format: email
        bcc:
          oneOf:
          - type: string
            format: email
          - type: array
            items:
              type: string
              format: email
        subject:
          type: string
        plainBody:
          type: string
        htmlBody:
          type: string
        replyToMessageId:
          type: string
          format: uuid
        labels:
          $ref: '#/components/schemas/Labels'
        sendAt:
          type: string
          format: date-time
          nullable: true
          description: 'Schedule the send: ISO 8601 with an offset, e.g.

            `2026-10-01T09:00:00-04:00`. Must be in the future and at most 90

            days ahead. A scheduled draft needs at least one recipient and a

            body. On update, `null` cancels the schedule. Anjal checks for due

            drafts about every 20 seconds. If the send fails, the draft is

            kept, `sendError` is set, `sendAt` is cleared and

            `draft.send_failed` fires.

            '
          example: '2026-10-01T09:00:00-04:00'
    Labels:
      description: 'Labels as an array or a comma-separated string. Stored lowercase and

        deduplicated; at most 32 labels of at most 64 characters, no commas.

        '
      oneOf:
      - type: array
        maxItems: 32
        items:
          type: string
          maxLength: 64
      - type: string
      example:
      - needs-review
      - q4-campaign
    Draft:
      type: object
      properties:
        id:
          type: string
          format: uuid
        inboxId:
          type: string
          format: uuid
        to:
          type: array
          items:
            type: string
            format: email
        cc:
          type: array
          items:
            type: string
            format: email
        bcc:
          type: array
          items:
            type: string
            format: email
        subject:
          type: string
        plainBody:
          type: string
          nullable: true
        htmlBody:
          type: string
          nullable: true
        replyToMessageId:
          type: string
          format: uuid
          nullable: true
        labels:
          type: array
          items:
            type: string
          description: Applied to the message when the draft is sent.
        sendAt:
          type: string
          format: date-time
          nullable: true
          description: When Anjal will send this draft. Null if not scheduled.
        sendError:
          type: string
          nullable: true
          description: Why the last scheduled send failed. The draft is kept and `sendAt` is cleared.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
          example: VALIDATION_ERROR
      required:
      - error
  parameters:
    InboxId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Inbox UUID
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: 'Unique value for this send (e.g. a UUID). A retry with the same key and

        identical request returns the stored response (header

        `Idempotent-Replayed: true`) and sends nothing. Same key with a

        different request: 409 `IDEMPOTENCY_KEY_REUSED`. Same key while the

        first is still running: 409 `IDEMPOTENCY_IN_PROGRESS`. Blank: 400

        `IDEMPOTENCY_KEY_INVALID`. Keys are per account and expire after 24

        hours; a failed send (non-2xx) frees the key.

        '
    OffsetQuery:
      name: offset
      in: query
      schema:
        type: integer
        minimum: 0
        default: 0
    DraftId:
      name: did
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Draft UUID
    LimitQuery:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
  responses:
    IdempotencyConflict:
      description: '`IDEMPOTENCY_KEY_REUSED` (the key was used for a different request) or

        `IDEMPOTENCY_IN_PROGRESS` (the first request with this key is still running).

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationError:
      description: Request body failed schema validation
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Any of `sa_`, `tk_`, `wk_`, `ak_`, or `mam_` keys. Scope is enforced

        per-endpoint — see individual operation descriptions.

        '