> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noetive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a message

> Compose and send, or reply. The reply form computes recipients from the
message being replied to rather than making the caller reconstruct
them from a rendering: a rendering is partly what a sender chose.

A send is subject to the mailbox's limits and policy. Three outcomes
are normal and distinguishable: it is `queued`, it is `held`, or it is
refused with `422 policy_refused` and `guard` naming the rule that
fired. Neither a hold nor a refusal is worth retrying unchanged.
Retrying is how one held message becomes five.

No agent can approve a held message in this deployment (see
`hold.update`), so a hold ends in rejection when it expires, after
72 hours unless the account set otherwise. A mailbox created on a
customer's first request has nobody to approve its mail, so what would
be held for it is refused instead, and the agent learns at once which
rule fired.

Pass `idempotency_key` on every send. Without one, a timeout you retry
is a second message.




## OpenAPI

````yaml /api/bud-api.yaml post /v1/send
openapi: 3.0.3
info:
  title: Noetive Bud API
  version: 0.1.0
  description: >
    Noetive Bud is a managed mail, calendar and addressbook service whose

    first-class users are agents. People reach the same data through ordinary

    mail and calendar clients; a program reaches it through this API.


    All request and response bodies are JSON. Every operation is a `POST`,

    including the ones that only read. The input is a document rather than a

    query string, so a caller assembles a typed body instead of encoding

    filters into a URL. `/v1/watch` accepts a JSON `POST` and upgrades the

    response to a Server-Sent Events stream.


    ## Four operations over one grammar


    Underneath the operation names below there are four verbs: **wait**,

    **read**, **put** and **send**, over one uniform way of naming things.

    That is why the surface does not grow an operation every time it grows a

    kind of object. A client that has learned the grammar can reach what was

    added after it was written.


    `POST /v1/help.describe` returns the grammar itself, and

    `POST /v1/catalog.describe` returns what this deployment serves. Both are

    cheap and neither needs anything but a valid credential.


    ## Identity and scope


    Every request carries a bearer token. A token belongs to an agent, an

    agent belongs to one account, and an agent reads and writes its own

    mailbox, calendar and addressbook. A grant may extend that to another

    agent's, never beyond the account.


    There is no operation on this surface that creates an account, an agent or

    a token. Those are administrative and are not reachable with an agent

    credential.


    ## Errors


    A refusal is a JSON object with an `error` member, at the HTTP status the

    code maps to. The status is the part to branch on; the code is the part to

    branch within.


    | code | status | meaning |

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

    | `invalid` | 400 | The request is malformed or a field is wrong. `field` is
    a JSON pointer to it. |

    | `unauthorized` | 401 | The credential is missing, unknown, revoked or
    expired. |

    | `not_billable` | 402 | The credential is good and the account behind it
    cannot incur usage. |

    | `forbidden_scope` | 403 | The credential is valid and does not reach this
    object, or does not reach an agent on this service at all. |

    | `not_found` | 404 | No such object, or no such operation. |

    | `precondition_failed` | 409 | Someone else wrote first, or a version was
    needed and not sent. `version` is the value to retry with. |

    | `policy_refused` | 422 | A policy declined. The message says which. |

    | `rate_limited` | 429 | A limit is spent. `retry_after_ms` says how long to
    wait. |

    | `paused` | 503 | The mailbox is paused and is not sending. |

    | `upstream_unavailable` | 503 | Something this request needed did not
    answer. Retry with backoff. |

    | `unavailable` | 501 | This deployment does not implement the operation. Do
    not retry it. |

    | `internal` | 500 | Something went wrong that is not about the request. |


    Every response, successful or not, carries an `X-Request-Id` header, and a

    refusal repeats it as `error.request_id`. Quote it in a support request: it

    correlates the refusal with everything the request caused.


    `unavailable` is deliberately 501 rather than 500. It means this build

    never serves the operation without a new deployment, and retry policies

    are written against statuses.


    ### Only 401 means the credential is the problem


    Four refusals concern the credential's surroundings rather than the

    credential, and only one of them is worth minting a new key over.


    Treat **401** as "this key never works": stop presenting it. `403`,

    `402` and `upstream_unavailable` all mean the key itself is fine. It

    reaches no agent here, or the account cannot currently incur usage, or

    something the request needed did not answer. A client that reacts to those

    by discarding the key throws away a working credential and gets no further,

    which is why they are separate statuses and why `WWW-Authenticate` is sent

    with the 401 and with nothing else. Branch on the header's presence if that

    is easier than branching on the status.


    `upstream_unavailable` is the one to retry. It is transient by definition,

    and the request had no effect.


    ## Limits


    | limit | value |

    | --- | --- |

    | request body | 1 MiB, or 16 MiB for a blob create carrying its bytes
    inline |

    | JSON nesting depth | 64 |


    A body over the limit is refused as `invalid` with a JSON envelope, not as

    a bare transport error, so one error path covers size and shape alike.


    ## Concurrency


    An operation that changes a stored object takes a `version` and returns the

    new one. Supplying the version you read is what makes an update a

    compare-and-swap. Omitting it for an object that already exists is

    `precondition_failed`, the same as a lost race, and both carry the

    `version` to retry with. There is no blind overwrite, with one exception:

    `message.update` changes read state, labels and folder without a version,

    and its `labels` replace the message's labels whole, so two agents

    labelling one message at once keep only the last write.


    ## Idempotency


    `POST /v1/send` and the create, update and delete operations accept

    `idempotency_key`. A repeat with the same key returns the first attempt's

    result rather than doing the work again. A repeat that arrives while the

    first attempt is still running is refused as `rate_limited`, with

    `retry_after_ms` saying when to ask again. A key belongs to the agent that

    sent it, and a refused or failed attempt does not spend it.


    | operation | a repeat with the same key |

    | --- | --- |

    | `send` | returns the first send's result for seven days, through restarts,
    deployments and outages |

    | every other write | is recognised on a best-effort basis for up to an hour
    |


    Outside `send`, a key can be forgotten sooner: after a service

    interruption, or while the service is under heavy load. A forgotten key

    does the work again, exactly as if no key had been sent; it never returns

    the result of a different request. Treat the key as protection against the

    retry you make straight after a timeout, and make a retried create safe to

    repeat: read before retrying, or delete the extra object.


    A key names one request. Never reuse one for a different request: on a

    create, update or delete such a reuse is refused as `invalid`.
servers:
  - url: https://bud.noetive.io
    description: Bud endpoint
security:
  - bearerAuth: []
tags:
  - name: Mail
    description: Read, file and reply to messages.
  - name: Send
    description: Compose and send, with drafts and approval holds.
  - name: Calendar
    description: Calendars, events and free/busy.
  - name: Contacts
    description: Addressbooks and contacts.
  - name: Attachments
    description: Parts of received messages, and blobs to attach to sent ones.
  - name: Events
    description: The live stream of what happened.
  - name: Discovery
    description: The grammar, what this deployment serves, and who you are.
  - name: Health
    description: Liveness, and whether a credential is accepted.
paths:
  /v1/send:
    post:
      tags:
        - Send
      summary: Send a message
      description: |
        Compose and send, or reply. The reply form computes recipients from the
        message being replied to rather than making the caller reconstruct
        them from a rendering: a rendering is partly what a sender chose.

        A send is subject to the mailbox's limits and policy. Three outcomes
        are normal and distinguishable: it is `queued`, it is `held`, or it is
        refused with `422 policy_refused` and `guard` naming the rule that
        fired. Neither a hold nor a refusal is worth retrying unchanged.
        Retrying is how one held message becomes five.

        No agent can approve a held message in this deployment (see
        `hold.update`), so a hold ends in rejection when it expires, after
        72 hours unless the account set otherwise. A mailbox created on a
        customer's first request has nobody to approve its mail, so what would
        be held for it is refused instead, and the agent learns at once which
        rule fired.

        Pass `idempotency_key` on every send. Without one, a timeout you retry
        is a second message.
      operationId: send
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendRequest'
      responses:
        '200':
          $ref: '#/components/responses/Send'
        '400':
          $ref: '#/components/responses/Invalid'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/NotBillable'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/PolicyRefused'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/Paused'
components:
  schemas:
    SendRequest:
      type: object
      required:
        - to
      properties:
        to:
          type: array
          items:
            type: string
          description: Recipients.
        cc:
          type: array
          items:
            type: string
        bcc:
          type: array
          items:
            type: string
        from:
          type: string
          description: |
            Which of the agent's addresses to send as. Empty takes its
            primary address.
        on_behalf_of:
          type: string
          description: |
            Send as another agent, which needs a grant. The message then
            names both, so a recipient can see who wrote it and who it is
            from, and policy usually holds it for approval, which is the
            point of the distinction.
        subject:
          type: string
        text:
          type: string
          description: The plain-text body.
        html:
          type: string
          description: |
            An alternative to `text`, not a replacement for it. A message with
            no plain part is one the reader on the other side has to render
            before it can read it.
        in_reply_to:
          type: string
          description: |
            The message this replies to. It is what puts the reply in a
            conversation.
        reply_all:
          type: boolean
          description: |
            Compute recipients from the message being replied to, minus the
            caller's own addresses.
        idempotency_key:
          type: string
          description: |
            Strongly recommended. A repeat with the same key returns the first
            attempt's result rather than sending a second message.
    SendResult:
      type: object
      properties:
        state:
          type: string
          enum:
            - queued
            - held
            - rejected
          description: |
            `queued` means accepted for delivery. `held` means it is waiting on
            an approval and must not be retried. `rejected` means it is not
            sent.
        message:
          type: string
          description: The message's identifier, once it has one.
        reason:
          type: string
          description: Why, when it is held or rejected.
        error:
          $ref: '#/components/schemas/Error'
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/Error'
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - invalid
            - unauthorized
            - not_billable
            - forbidden_scope
            - not_found
            - precondition_failed
            - policy_refused
            - rate_limited
            - paused
            - upstream_unavailable
            - unavailable
            - internal
        message:
          type: string
          description: What went wrong, in one sentence.
        hint:
          type: string
          description: What to do about it.
        field:
          type: string
          description: |
            A JSON pointer to the offending field, on `invalid`.
          example: /id
        request_id:
          type: string
          description: |
            Correlates this refusal with everything the request caused. The
            same value is on the `X-Request-Id` header. Quote it in a support
            request.
        retry_after_ms:
          type: integer
          description: How long to wait, on `rate_limited`.
        version:
          type: string
          description: The version to retry with, on `precondition_failed`.
        current:
          type: object
          description: |
            Sometimes present on `precondition_failed`: the object as stored.
            Do not rely on it; read the object again when it is absent.
  responses:
    Send:
      description: |
        The send was accepted, held, or refused. Read `state`: a `200` here
        does not mean the message left.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SendResult'
    Invalid:
      description: The request is malformed, or a field is wrong.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: |
        The credential is missing, unknown, revoked or expired. These are one
        answer on purpose: the difference between them is information a caller
        has not earned.

        This is the only refusal that means the credential itself does not
        work. Stop presenting it.
      headers:
        WWW-Authenticate:
          schema:
            type: string
          description: |
            Present so a client knows to fetch a token rather than retry. Sent
            with this status and with no other, so its presence is a reliable
            test for "the credential is the problem".
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotBillable:
      description: |
        The credential is good and the account behind it cannot incur usage.
        Nothing about the key needs changing, and retrying does not help until
        the account does.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: |
        The credential is valid and does not reach what was asked for, either
        this object, or any agent on this service. The key is fine; its reach is
        not.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    PolicyRefused:
      description: A policy declined. The message says which.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: A limit is spent. `retry_after_ms` says how long to wait.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Paused:
      description: |
        The mailbox is paused and is not sending.

        Two codes share this status on this operation, and the `code` member is
        what tells them apart: `paused` is a decision somebody made about this
        mailbox and does not change on retry, while `upstream_unavailable` means
        something the request needed did not answer and is worth retrying. This
        is the case the error table means by "branch on the status, then within
        it on the code".
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Bearer token for one agent. Pass as
        `Authorization: Bearer <token>`.

        A token is shown once when it is issued and is not recoverable
        afterwards. Treat it as a secret: it reaches one agent's mail,
        calendar and contacts, and anything a grant has extended to it.

````