# Async Video

Video generation is asynchronous - submit returns a `task_id`, the
result arrives seconds-to-minutes later. Task statuses are uppercase
strings: `SUBMITTING`, `PROCESSING`, `SUCCESS`, `FAILED`.

## Pattern 1: Poll (recommended)

    # Submit
    TASK=$(curl -s -X POST https://tokpum.com/v1/video/generations \
      -H "Authorization: Bearer sk-xxxx" \
      -H "Content-Type: application/json" \
      -d '{"model":"minimax-h3","prompt":"...","aspect_ratio":"16:9","duration":5,"resolution":"768p"}' \
      | jq -r .task_id)

    # Poll
    while true; do
      RESP=$(curl -s "https://tokpum.com/v1/video/tasks/$TASK" \
        -H "Authorization: Bearer sk-xxxx")
      STATUS=$(echo "$RESP" | jq -r .status)
      case $STATUS in
        SUCCESS) VIDEO_URL=$(echo $RESP | jq -r .result.video_url); break ;;
        FAILED)  echo "failed: $RESP"; exit 1 ;;
        *)       sleep 5 ;;
      esac
    done

The poll response is `{task_id, status, progress, result, error}`;
`result` (`{video_url, cover_image_url, duration, resolution}`) is only
present once status is `SUCCESS`. A task you don't own and a task that
doesn't exist are both the same 404 - ownership is never leaked.

## Pattern 2: Webhook callback

Pass `webhook_url` in the submit body:

    curl -X POST https://tokpum.com/v1/video/generations \
      -H "Authorization: Bearer sk-xxxx" \
      -H "Content-Type: application/json" \
      -d '{"model":"minimax-h3","prompt":"...", "webhook_url": "https://your-agent.example.com/tokpum-webhook"}'

When the task terminates (SUCCESS, FAILED, or accepted cancel), the
gateway fires a terminal callback event for your `webhook_url`
carrying the `task_id` and final status. Delivery is best-effort:
treat the webhook as a wake-up signal, then re-fetch
`GET /v1/video/tasks/{task_id}` for authoritative state. Callbacks are
not currently signed - do not trust callback contents as
authentication; the poll endpoint (Bearer-authenticated) is the source
of truth.

## Idempotency

A duplicate submit with the same `(model, prompt, params)` does NOT
return the same `task_id` - each submission creates a new task and a
new charge. To deduplicate, store the returned `task_id` with your own
idempotency key in your database.

## Cancellation

`POST /v1/video/tasks/{task_id}/cancel` accepts cancellation only in
the `PROCESSING` state and only if the upstream still has the task
queued (e.g., MiniMax refuses to cancel a running task). Outcomes:

- Upstream accepted - task transitions to `FAILED` with reason
  `merchant_cancelled` and the hold/refund is applied automatically
- Task already `SUCCESS` or still `SUBMITTING` - 409, no side effect
- Upstream refused (task already running) - 409, the task continues
  and will be charged; wait for it or accept the cost
- Task already `FAILED` - 200 with status `FAILED`, no-op

## Presigned URLs

`result.video_url` is a presigned S3 URL with a deployment-configured
TTL. `GET /v1/video/tasks/{task_id}` re-presigns a fresh URL on every
fetch - if a stored link expires, just GET the task again and use the
new `video_url`.
