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

# Cancel an inbound request

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

## Request

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

Discards the draft an inbound email produced. The shared executor voids the envelope with a compare-and-set and moves the conversation to `cancelled` through the state machine, so the audit row is written once and the same draft can never be cancelled twice. No mail goes out and no credit is spent or refunded.

**Preconditions.** `awaiting_approval` is the only actionable state; anything else answers **400 `invalid_status`** with `details.state`. A draft that has already been sent or cancelled underneath the caller answers the same 400. If the envelope the request created is gone entirely, the answer is 409 `envelope_missing`. Note the split again: this is 400 `invalid_status`, where archive and reminders use 409 `invalid_state`.

Cancelling is the void capability, so a credential that may not void this workspace's envelopes gets 403 while a request in another workspace gets 404.

**Webhooks.** `cancelled` is not `rejected`: neither `email_envelope.approved` nor `email_envelope.rejected` fires here. `email_envelope.rejected` belongs to conversations the mail agent itself refused.

No request body. Honors `Idempotency-Key`.

## Headers

- `Idempotency-Key` string

## Response `200`

The settled conversation, re-read after the transition, with `state: "cancelled"`.

- 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`, or its draft has already been sent or cancelled. `details.state` names the state.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `403`: The credential is valid but may not do this. Codes: `insufficient_scope` (the route's scopes are not all held; `details` names `requiredScopes` and `missingScopes`), `forbidden` (the capability check on the resolved resource failed), `scope_escalation` (a grant wider than the caller's own was requested).
- `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`: `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`: The request parsed but its content is not acceptable. Codes: `validation_error` (envelope configuration, with an `issues` array in `details`), `invalid_envelope`, `envelope_not_ready`, `document_page_limit`, `idempotency_key_reuse`.
- `429`: A limit was reached. Codes: `rate_limited` (the credential's per-bucket HTTP limit, or the per-workspace detection throttle), `workspace_limit_exceeded` (a workspace content or send limit; `details.limit` names it), `daily_budget_exceeded` (the workspace's daily detection budget). The four rate-limit headers are sent on this response and on no successful one, so a client cannot read its remaining budget without being refused. Retrying after `Retry-After` is always safe: a rate-limit refusal happens before the handler runs.
- `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)
