---
title: "Answer the decision a drop is waiting on"
method: POST
path: "/v1/engine/runs/{id}/approve"
tags: ["Drops"]
---

# Answer the decision a drop is waiting on

`POST /v1/engine/runs/{id}/approve`

## Request

- Base URL: `https://aicontentdrop.com`
- URL: `https://aicontentdrop.com/v1/engine/runs/{id}/approve`
- Auth: HTTP bearer

Answer the decision a drop is waiting on. This is the ONLY endpoint that lets a drop charge credits: pass the `task_id` and `quote_id` exactly as the drop gives them in `pending_approval`, with decision `approve` to let that one step run or `decline` to cancel it. The quote fixes the credit ceiling, credits are charged only if the step succeeds, and approving the same quote twice does nothing the second time. A quote that is not the one on the step is refused, and so is one whose price has lapsed — in that case nothing is released and the engine prices the step again by itself, so read the drop for the new `quote_id`.

## Path parameters

- `id` string, uuid, required

## Request body

- object
  - `task_id` string, uuid, required: The step waiting on a decision.
  - `quote_id` string, required: The priced quote for that exact step. Refused if it is not the one on the step or its price has lapsed.
  - `decision` 'approve' | 'decline'
  - `revision` integer: Optional. When given it must be the drop's current revision.

## Response `200`

The decision was recorded. On approve the step runs in the background; on decline it is cancelled and nothing is charged.

- DropApproved: The drop and the step, after the decision was recorded.
  - `run` DropRun, required: One drop, as every caller sees it. `phase` is a property of the drop, never a place to navigate to; `revision` is bumped by every write and is what a conditional write is checked against.
    - `id` string, uuid, required
    - `title` string, required
    - `goal` string, required
    - `phase` 'intake' | 'research' | 'concept' | 'plan' | 'produce' | 'assemble' | 'distribute' | 'done', required
    - `status` 'idle' | 'running' | 'waiting_input' | 'waiting_approval' | 'failed' | 'done' | 'cancelled', required
    - `lane` 'cinematic' | 'ugc' | 'marketing' | 'null', nullable: Which recipe this drop follows. Null while the lane is still a question.
    - `approvalMode` 'confirm_each', required: confirm_each is the only mode served: the drop stops before every step that would charge credits.
    - `budgetCredits` integer, nullable, required: The credit ceiling for the whole drop, or null for none. Approving a step cannot lift it.
    - `creditsQuoted` number, required
    - `creditsSpent` number, required: Credits charged so far. Work that failed charges nothing.
    - `revision` integer, required
    - `canvasId` string, nullable, required: The board this drop builds its artifacts on, once it has one.
    - `error` string, nullable
    - `entities` DropEntities, required: Everything a drop has made, keyed by kind and then by id. An artifact enters once and is referenced afterwards, so ids stay stable for the life of the drop. Instructions sent to a producer, internal model ids and job references are not part of this projection and never appear.
      - `brief` object: The settled brief: what is being made, for what, how long, on which recipe.
      - `videos` object
      - `images` object
      - `ads` object
      - `research` object: Competitor rows read for this drop. Each carries `demo` and `analysed` so a sample row is never shown as an observed one.
      - `concepts` object: Angles, each bound to the research ids it rests on.
      - `storyboards` object
      - `boards` object: The board this drop builds on, with its link.
      - `quotes` object
    - `counters` DropCounter[], required
      - `word` 'retrieved' | 'analysed' | 'selected' | 'generated' | 'checked', required
      - `count` integer, required: The number of ids below.
      - `label` string, required: What was counted, in the words a person reads.
      - `coverage` string: How it was measured, when the word alone would overstate it.
      - `bucket` 'brief' | 'videos' | 'images' | 'ads' | 'research' | 'concepts' | 'storyboards' | 'boards' | 'quotes', required: Which bucket of this drop's artifacts the ids live in.
      - `ids` string[], required
    - `shared` boolean, required: Whether this drop currently has a share link. The address itself is returned only to the POST that minted it.
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
  - `task` DropTask, required: One step of a drop. A step that can charge credits is parked in `waiting_approval` with `quoteId` set until it is answered; the inputs and outputs a producer saw are not part of this projection.
    - `id` string, uuid, required
    - `runId` string, uuid, required
    - `kind` 'research.ads' | 'research.decode' | 'concept.angles' | 'plan.storyboard' | 'plan.board' | 'produce.video' | 'produce.image' | 'produce.ad' | 'produce.ugc' | 'produce.avatar' | 'assemble.timeline' | 'distribute.schedule', required
    - `phase` 'intake' | 'research' | 'concept' | 'plan' | 'produce' | 'assemble' | 'distribute' | 'done', required
    - `status` 'queued' | 'leased' | 'running' | 'waiting_approval' | 'succeeded' | 'failed' | 'cancelled', required
    - `quoteId` string, nullable, required
    - `creditsUsed` number, required
    - `error` string, nullable, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required

## Other responses

- `400`: The body was rejected. `code` names the class of problem and `details` names the fields.
- `401`: Missing, malformed, revoked, or budget-exhausted API key
- `404`: No drop with that id on this account — or the whole surface is absent, which is what an account that is not enabled for drops sees. Drops are available to accounts enabled for them; on those accounts every path below answers `{"error":{"code":"not_found"}}` and nothing else, so an unlisted caller cannot tell an enabled surface from an absent one. On an enabled account, this is a drop id that does not exist or is not yours.
- `409`: The drop is not in a state that allows this. `code` says which: `RUN_BUSY` (it is working on the last message), `RUN_FINISHED`, `REVISION_CONFLICT` (it changed while your request was in flight — read it again and repeat), `TASK_NOT_AWAITING_APPROVAL`, `QUOTE_UNKNOWN` (that step has no priced quote yet), or `QUOTE_EXPIRED` (the price lapsed, so nothing was released).
- `429`: Rate limited. Read Retry-After (header) or error.retry_after_seconds (body) — they carry the same number — and wait that long. Retrying sooner extends the window rather than shortening it.
- `500`: Unexpected failure on our side. Retry once; nothing was charged.

---

[API](https://skmtc.dev/aicontentdrop/apis/ai-content-drop-api.md) · [All operations](https://skmtc.dev/aicontentdrop/apis/ai-content-drop-api/llms.txt) · [OpenAPI document](https://skmtc.dev/aicontentdrop/apis/ai-content-drop-api/revisions/75a13a8043ae?raw)
