---
title: "Approve an inbound request"
method: POST
path: "/inbound-requests/{requestId}/approve"
tags: ["Inbound Requests"]
---

# Approve an inbound request

`POST /inbound-requests/{requestId}/approve`

## Request

- Base URL: `https://inkseal.net/api/v1`
- URL: `https://inkseal.net/api/v1/inbound-requests/{requestId}/approve`
- Auth: HTTP bearer

Approves the draft envelope an inbound email produced, through the same executor as the approval link in the confirmation mail and the reply keyword.

**This is an agent send, not a review.** The link and the keyword are a person approving the draft they were shown. A credential calling this endpoint is sending on its own authority, so it answers to every agent guardrail a direct `sendEnvelope` does: it is refused while the workspace requires send approval or while the draft has an open send approval, refused when any signer or CC is on the deny list, counted against this credential's own spend limits, and recorded as sent by this credential.

What it actually creates: nothing new. The draft already exists (the mail agent converted the chosen attachment into a document and built the envelope from the parsed intent before parking the request). Approval claims that draft with a compare-and-set, hands it to the same send worker `POST /envelopes/{envelopeId}/send` uses, and moves the conversation to `approved` through the state machine so its audit row and lifecycle webhook fire exactly once.

**Asynchronous, like a direct send.** The 202 means the draft was claimed and queued; the worker owns the single credit charge and the delivery. Poll `GET /envelopes/{envelopeId}` on the returned `envelope.id`, or subscribe to `envelope.sent` / `envelope.send_failed` / `envelope.completed`.

**Preconditions.** `awaiting_approval` is the only actionable state. Any other state answers **400 `invalid_status`** with `details.state`. Note the split: this endpoint uses 400 `invalid_status` where the archive and reminder endpoints use 409 `invalid_state` for a comparable refusal.

**Webhooks.** Reaching `approved` fires `email_envelope.approved`. `email_envelope.rejected` is its counterpart for a conversation the agent rejected (a refused or unusable message), not for a cancellation: `POST .../cancel` moves the request to `cancelled` and fires neither.

Two concurrent approvals are safe without an `Idempotency-Key`: one claims the draft and the other is told it has already moved. With a key, the retry replays the first response verbatim.

No request body. Metered in the `send` rate bucket, and costs `envelopes:send` for the same reason a direct send does.

## Headers

- `Idempotency-Key` string

## Response `202`

The draft was claimed and queued. The conversation is re-read after the transition, so `state` is `approved` and `envelope.id` names the envelope now sending.

- object
  - `data` InboundRequest, required: One inbound mail conversation as the API publishes it: the workflow, not the mail. No bodies, no storage keys, no reply token, no provider message ids, no row ids.
    - `id` string, required
    - `state` 'received' | 'parsing' | 'parsed' | 'drafted' | 'awaiting_approval' | 'approved' | 'cancelled' | 'expired' | 'needs_info' | 'rejected' | 'failed', required: Where the conversation is. `awaiting_approval` is the only actionable state. `approved`, `cancelled`, `expired`, `rejected`, and `failed` are terminal. `needs_info` means the agent has emailed the sender for missing detail and is waiting.
    - `error` string, nullable, required: Why the request failed or was rejected, or a non-fatal warning recorded while drafting. Null when nothing went wrong.
    - `intent` InboundRequestIntent, required: What the parser understood from the email. Nothing the model was given is echoed back, and a stored intent whose shape this API no longer recognizes is published as null rather than forwarded verbatim.
      - `signers` object[], required
        - `name` string, nullable, required
        - `email` string, email, required
        - `order` integer, nullable, required: 1-based position for a sequential send, or null when the sender did not order them.
      - `signingOrder` 'parallel' | 'sequential', required
      - `subject` string, nullable, required
      - `message` string, nullable, required
      - `expiresInDays` integer, nullable, required
      - `attachmentIndex` integer, nullable, required: Which attachment the sender chose, by position in `attachments`.
      - `notes` string, nullable, required: Anything the parser recorded that does not fit the other fields.
    - `missingInformation` string[], required: What the parser could not determine. This is the field to branch on when deciding whether to reply to the sender with the missing detail.
    - `attachments` InboundRequestAttachment[], required
      - `filename` string, required
      - `contentType` string, required
      - `size` integer, required: Bytes stored, or the size the provider declared for a refused file.
      - `status` 'stored' | 'refused', required
      - `refusedReason` 'null' | 'too_many_attachments' | 'file_too_large' | 'message_too_large', nullable, required: Why the file was skipped. Null on a stored attachment.
    - `document` InboundRequestResourceRef, required: A resource this conversation produced, by public id.
      - `id` string, required
    - `envelope` InboundRequestResourceRef, required: A resource this conversation produced, by public id.
      - `id` string, required
    - `clarificationRoundTrips` integer, required: How many `needs_info` rounds the conversation has used. The loop is capped, and hitting the cap fails the request.
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required

## Other responses

- `400`: `invalid_status`: the request is not `awaiting_approval` (`details.state` names the state), or its draft has already been sent or cancelled. Also the malformed-body and malformed-`Idempotency-Key` cases.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `402`: `insufficient_credits`. The workspace balance does not cover what the envelope costs; the request stays `awaiting_approval`.
- `403`: `sender_unauthorized`: the originating sender no longer has workspace authority. Also `forbidden`, `insufficient_scope`, and `recipient_denied` for credential capability, scope, or recipient policy refusals.
- `404`: The resource does not exist, was deleted, or belongs to another workspace. Code: `not_found`. The three cases are answered identically on purpose, so a 404 never confirms that an identifier exists elsewhere.
- `409`: `approval_required`: the workspace requires a person to approve agent sends, or the draft has an open send approval that a person has to decide. The owner approves from the email they were sent. `envelope_missing`: the draft this request created is no longer available. The same status also covers an `Idempotency-Key` another request is still working.
- `422`: `invalid_envelope`: the draft is not sendable as it stands (the message names the first contract problem). `recipient_denied`: a signer or CC is on the workspace's agent deny list; `details.recipients` names each one and the entry that matched. Also `idempotency_key_reuse`.
- `429`: The `send` rate bucket is exhausted (`rate_limited`), or the workspace hit a send ceiling (`workspace_limit_exceeded`, with `details.limit`). `credential_limit_exceeded` when the calling credential has reached its own spend limit; nothing is sent and the draft is left alone.
- `503`: A dependency the request needs is unreachable and nothing was changed. Codes: `queue_unavailable` (a background job could not be enqueued; retry the call), `billing_unavailable` (Stripe would not open a checkout session; nothing was charged, retry the call), `service_unavailable` (the rate-limit store is down, so the request was refused before running).

---

[API](https://skmtc.dev/inkseal/apis/inkseal-api.md) · [All operations](https://skmtc.dev/inkseal/apis/inkseal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/inkseal/apis/inkseal-api/revisions/c440c981d578?raw)
