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

# Summer API jobs: estimate, run, batches, follow, webhooks and download

> How a Summer API job works: price it with POST /v1/estimate, start it with POST /v1/jobs or up to 500 at once with POST /v1/batches, follow it with GET /v1/jobs/{id}, server-sent events or a signed webhook, then download the result. No rate limits on paid usage.

Every model runs the same way: one job with a `model` and an `input`. Short jobs finish inside the call. Long jobs, such as a 3D model or a video, give you a job id to follow.

## Know the price first

`POST /v1/estimate` takes the same body as a job and prices it: the exact amount, or the most it can cost when `atMost` is `true`. It is free and needs no sign-in. The answer has these fields at the top level, with no wrapper.

```bash theme={null}
curl https://www.summerengine.com/v1/estimate \
  -H "Content-Type: application/json" \
  -d '{"model": "3d/default", "input": {"kind": "text-to-3d", "prompt": "a stylized treasure chest, game prop"}}'
```

| Field | Meaning |
| - | - |
| `price` | US dollars, as a decimal string |
| `atMost` | `true` when Summer decides a step while the job runs, such as cleaning up a picture first. The charge can be lower, never higher. |
| `breakdown` | What makes up the price: `base`, then each option that adds to it, such as `rig` |
| `quoteId` | Send it with the job: Summer refuses to charge more than this quote |
| `quoteExpiresAt` | The quote holds for 15 minutes |

## Start a job

```bash theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model": "3d/default", "input": {"kind": "text-to-3d", "prompt": "a stylized treasure chest, game prop"}, "quoteId": "q_..."}'
```

| Part | Required | Meaning |
| - | - | - |
| `model` | Yes | A model id from [`GET /v1/models`](https://docs.summerengine.com/api/models), or a default such as `3d/default` |
| `input` | Yes | The model's input. Each model page lists its fields; `inputSchema` in `GET /v1/models/{id}` has the JSON Schema. |
| `quoteId` | No | From `POST /v1/estimate`. If the price went up since the quote, the job does not start and nothing is charged. |
| `Idempotency-Key` header | Yes | A new value for each new job, such as a UUID. Send the same value to retry the same job: you get the first answer, and nothing is charged twice. |
| `Prefer: wait=N` header | No | Hold the answer up to `N` seconds and return the finished job if it finishes in time. Values over 60 count as 60, so give your HTTP client a timeout above 60 seconds. |

The answer is `{"job": {...}}`:

* `201` with `state: "done"`: the job finished. `result` holds the work.
* `202` with `state: "running"`: still working. Follow `job.id`.
* `202` with `state: "queued"`: all your running slots are busy. The job is accepted at a held price and starts in order. Its id starts with `q:`; follow it like any job. Do not send it again.

The job also carries `model`, the model that ran (also when you sent a default id such as `3d/default`, so you can run the same model again), `estimatedPrice`, and `price` once Summer knows what it charged.

## Running slots and the queue

Paid work has no rate limits. Each account runs 10 jobs at once at the start, and up to 40 as it buys more credits. Every job past that answers `queued` and starts as soon as one of your running jobs finishes, in the order you sent them. Queued and running jobs hold their price against your paid balance and the app's spend limit until they are charged, so the queue never spends past either one.

The only refusals are about money, at once and before any work starts: `402` `no_credits` (the paid balance) or `402` `over_limit` (the app's own limit). A queued job that cannot start for 30 minutes while your queue stands still ends `failed`, with nothing charged.

## Run many jobs: batches

`POST /v1/batches` takes up to 500 jobs in one call, all or none. Every job is priced first. When your paid credits and the app's limit cover all of them, all are accepted (`202`) and run in order on your running slots. When they do not, nothing is accepted.

```bash theme={null}
curl https://www.summerengine.com/v1/batches \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"jobs": [
    {"model": "3d/default", "input": {"kind": "text-to-3d", "prompt": "a wooden barrel with iron bands, game prop"}},
    {"model": "image/default", "input": {"prompt": "a red health potion icon, flat fantasy style", "removeBackground": true}},
    {"model": "sound/default", "input": {"text": "glass potion bottle uncorked, short pop", "durationSeconds": 1}}
  ]}'
```

The answer is `{"batch": {"id": "b_...", "estimatedPrice": "...", "jobs": [{"id": "q:...", "state": "queued", "model": "...", "estimatedPrice": "..."}]}}`. Each job may carry its own `idempotencyKey` and `webhookUrl`. The header covers the whole batch: a retry with the same `Idempotency-Key` answers the batch it already made and never queues it twice.

A batch takes no `quoteId`. Each job is priced when the batch arrives and holds that price (`estimatedPrice`). To tell the person the total first, estimate each distinct request with `POST /v1/estimate` and add them up.

`GET /v1/batches/{batchId}` answers every job, in the order you sent them, with its state and, when done, its `result`, plus `counts` by state. Read it every 10 seconds or so until no job is `queued` or `running`, then download the files of the `done` jobs. Batches are kept 7 days.

A job with a wrong input refuses the whole batch with `invalid_input`; `param` names the job, such as `jobs[3]`. A batch that does not fit answers `402` with `details`: `fits` (how many jobs the money covers), `jobs`, `totalUsd` and `shortUsd` (how much more is needed). The code is `no_credits` when the paid balance is short and `over_limit` when the app's limit is.

With the CLI, a batch is a JSONL file with one job per line: `summer api run --batch jobs.jsonl --download assets`.

## Follow a job

```bash theme={null}
curl "https://www.summerengine.com/v1/jobs/$JOB_ID" -H "Authorization: Bearer $SUMMER_TOKEN"
```

Check again after `pollAfterSeconds` when the answer has it, or every 5 seconds. Job ids can hold a colon, such as `gen:...` or `q:...`; send them as they are. A queued job's read answers the job it became once it starts. With the CLI, `summer api jobs <id> --download` follows the job and saves its files.

| `state` | Meaning |
| - | - |
| `queued` | Waiting for one of your running slots. Its price is held. |
| `running` | Still working. `progress` (0 to 100) shows how far, when known. |
| `done` | Finished. `result` holds the work. |
| `failed` | It did not work. `error.code` and `error.message` say why. Nothing is charged. |
| `cancelled`, `expired`, `stale`, `declined` | It stopped without a result. Start a new job. |

Summer API jobs never wait for an approval (`awaiting_approval` is for other Summer jobs). Once accepted, queued or running, a Summer API job runs to the end: `POST /v1/jobs/{id}/cancel` answers `unsupported` for it. A job that fails costs nothing.

To get changes pushed instead, open the event stream. It sends a `job` event with `{"job": {...}}` each time the job changes, then `done` with `{"state": ...}` when the job reaches any final state, `failed` included. After about 4 minutes it sends `timeout`: open it again, or check the job.

```bash theme={null}
curl -N "https://www.summerengine.com/v1/jobs/$JOB_ID/events" -H "Authorization: Bearer $SUMMER_TOKEN"
```

## Webhooks

Pass `webhookUrl` (public `https`) when you start a job, and Summer sends one signed `POST` there when the job finishes. First make a signing secret, once per account:

```bash theme={null}
curl -X POST https://www.summerengine.com/v1/webhooks/secret -H "Authorization: Bearer $SUMMER_TOKEN"
```

The answer holds `secret`, shown only this once; keep it as `SUMMER_WEBHOOK_SECRET`. Making a new one stops the old one at once. Then add `"webhookUrl": "https://example.com/summer-hook"` to the `POST /v1/jobs` body; the job answer carries `webhookId`.

The `POST` body has the job's final facts:

```json theme={null}
{"type": "job.finished", "jobId": "gen:...", "model": "3d/hunyuan-3d", "status": "done", "createdAt": "...", "completedAt": "..."}
```

Then `GET /v1/jobs/{jobId}` for the files. Each `POST` carries `Summer-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `t`, a dot and the raw body, keyed with your secret. Check it before you trust the body:

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

Answer `2xx` to confirm. Summer retries after 1, 5 and 30 minutes, then 1, 2, 4, 8 and 24 hours; answer `410` to stop. `Summer-Webhook-Id` is the same on every retry, so you can drop repeats. `GET /v1/webhooks/deliveries` lists your newest deliveries with every attempt.

## The result

Every job has the same `result`: `assets`, one entry per file, each with `assetId`, `fileUrl` and `thumbnailUrl`. An image job with `numImages` above 1 has one entry per image. A job reads `done` once every file is in Summer storage, so every `fileUrl` is ready to download.

Assets made through the API are private: they show in your Summer library only. Each `fileUrl` is on Summer's own storage and downloads without a sign-in, so share a URL only where the file may be seen. Download the file into your project, or keep the asset id: in Summer Engine your agent imports an asset by its id.

## Send a picture

Image-to-3D, image editing and image-to-video take a public picture URL (`imageUrl`, `referenceImageUrl`). For a file on your computer, upload it first:

**1. Start the upload**

`POST /v1/files` with an empty JSON body (`{}`) answers with `uploadUrl`, the `fields` to send, `expiresAt`, and `example`: the whole upload as one curl command.

**2. Send the file**

POST every field as a multipart form field, plus the file as `file`, to `uploadUrl`. PNG, JPEG or WebP.

**3. Finish the upload**

`POST /v1/files/complete` with three values from the answer to step 2: `public_id` as `publicId`, `version`, and `signature` as `uploadSignature` (not the signature in `fields`). Add `format`: `png`, `jpg`, `jpeg` or `webp`. The answer holds `assetId`, `fileUrl`, `width` and `height`. The picture is a private asset in your Summer library: pass `fileUrl` as `imageUrl`.

The exact fields of each call are in the API reference: [Start a file upload](https://docs.summerengine.com/api/reference/post-files.md) and [Finish a file upload](https://docs.summerengine.com/api/reference/post-files-complete.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.