# Tasks and results

This reference describes the available Job Search API contract.

## Task state

Task lookup returns `{ "task": ... }`. Submission and cancellation return `{ "receipt": ..., "task": ... }`; the current task can be null while its command certainty remains recorded. A task includes `taskId`, `productId`, `status`, `createdAt`, `finishedAt`, `deadline`, `input`, `sources`, `contract`, `billing`, `initiatedBy`, `resultCount`, `sourceOutcomes`, `notices`, `coverageTruncated`, `resultExpiresAt`, `resultAvailability`, `failureCode` and `retryAfterSeconds`.

| Status | Meaning |
| --- | --- |
| `queued`, `running`, `finalizing`, `retry_wait` | Work is in progress. Results are not yet available. |
| `succeeded` | Accepted records have been committed. This is not a claim that all jobs at the source were collected. |
| `partial` | Usable records were committed with an incomplete source scope, as authorized by `allowPartial`. |
| `empty` | Completed scope yielded no accepted records; no collection charge. |
| `failed`, `expired`, `cancelled` | No result delivery; the reservation is released. |

`GET /v1/tasks/{taskId}` can return HTTP 200 for any task state. Inspect `status`, not just HTTP success. `failureCode` provides the task failure reason when applicable. `sourceOutcomes` and `coverageTruncated` identify coverage limits and are null before a receipt exists.

`notices` contains at most 50 collection status lines, each with `level` (`info`, `warning` or `end`) and a `message` of at most 300 characters. An empty array means no notices are currently supplied. Keep the level and render messages as untrusted text. Unsupported optional filters may be reported here.

Retained task `input` is the frozen JSON submitted under that task's schema version. Historical tasks may contain earlier fields. To start new work, construct an input using the current Job Search contract and obtain a new quote.

`billing.reservedUnits` and `billing.chargedUnits` are decimal strings, with 1,000,000 units per credit. A terminal task has no remaining reservation. `contract` records the accepted schema version, price version and total record limit across selected platforms.

### Reservation breakdown

The task response includes `billing.reservationTotalUnits`, the original accepted reservation, and `billing.releasedUnits`, the amount released from that reservation. A missing or null value means the breakdown is unavailable; do not treat it as zero.

For example, a task that initially reserved 10 credits and delivered 6 chargeable records at 1 credit each finishes with 0 reserved, 6 charged and 4 released. While a task is active, released units remain zero. Cancellation, expiry or failure without delivery releases the full reservation. Internal validation tasks reserve and charge zero.

When the full breakdown is known, `reservationTotalUnits = reservedUnits + chargedUnits + releasedUnits`. Use integer arithmetic on the decimal strings. Later refunds or result-loss compensation are separate ledger entries and do not rewrite this task's original charge or release.

### First attempt time

The task response includes `startedAt`, the database timestamp when the first execution attempt was allocated. It stays fixed across retries. This includes preparation before collection; it is not a timestamp from the source website or proof that the collector has started a network request.

`startedAt` is null before an attempt is allocated or when the first attempt's historical evidence is unavailable. Do not substitute a later retry, the creation time or the current time. `createdAt` to `finishedAt` remains the total task duration, including queue time and retries. The existing `deadline` field is the task's execution deadline.

### Running observations

The task contract includes nullable `progress` while a task is `running` or `finalizing`. It contains the current `attempt`, `phase` (`candidates`, `details` or `packaging`), `sourcesFinished`, `sourcesTotal`, `candidatesSeen`, `detailsProcessed`, `recordsMatched` and `receivedAt`.

These are provisional observations from the collector. `sourcesFinished` counts processed candidate lists, including failed checks. Matching records can still fail validation or be discarded before delivery. Use the final receipt's `resultCount` and billing fields for delivered records and charges. There is no completion percentage or estimated finish time.

`receivedAt` is when the API received the report; collection happened earlier, and a slow source may leave the counts unchanged. Missing observations are null, not zero. A new attempt clears the previous observation; queued, retrying and terminal tasks return null. This data does not extend the task deadline or result retention.

### Task navigation

The task contract includes `links` to task responses from REST, Console and MCP. Each relation contains `href` and `method`:

| Relation | Method | Purpose |
| --- | --- | --- |
| `self` | `GET` | Read the current task state. |
| `results` | `GET` | Read a result page, with optional fields and view parameters. |
| `download` | `POST` | Request download delivery; send a JSON body such as `{"format":"jsonl"}`. |
| `cancel` | `POST` | Request cancellation with full-access credentials. A completed task stays final. |

Resolve these paths against your configured API origin, including when the task came from Console or MCP. They contain no credentials or signed storage tokens. Supply the credentials required by the API; the presence of a link does not grant permission or promise that results are ready. Results can still return `409 RESULT_NOT_AVAILABLE` or `410 RESULT_EXPIRED` / `RESULT_LOST`. Follow a new task read to see the current state. Obtaining a link never starts another collection.

## Results

`GET /v1/tasks/{taskId}/results` returns `data` and `meta`, containing `resultCount` and `pagination: { hasMore, nextCursor, prevCursor }`. It does not embed records inside the task response. The default page size is 100. Optional `pageSize` is 1–1,000; a byte limit can produce a shorter page. Always follow `hasMore` and `nextCursor`.

Use `fields=title,companyName,platformUrl` to select fields. Keep the same field selection when continuing with a cursor. Cursors are opaque and bound to the account, task and saved snapshot. Pagination reads existing results without another collection or charge. Records follow the [Job Search output schema](/docs/job-search/#records), including nullable structured fields.

`resultAvailability` is `available`, `not_ready`, `expired` or `lost`. Once `resultExpiresAt` passes, result reads return `410 RESULT_EXPIRED`; the task itself can still be inspected. A new collection requires a new request and budget.

### Bounded model view

Result reads support `format=markdown` for bounded model consumption. The default remains `format=json`, with complete projected records.

For example, read `/v1/tasks/{taskId}/results?format=markdown&fields=recordId,title,description&maxItems=20&maxChars=12000`. The response contains `data` fragments with `markdown`, `offset`, `nextOffset` and `complete`. Each fragment labels collected text as untrusted source data and presents it as indented JSON text. Source instructions do not authorize tool calls or spending.

| Option | Markdown behavior |
| --- | --- |
| `fields` | Select from the product's advertised `modelView.fields`. Keep the selection unchanged on continuation. |
| `maxItems` | Maximum fragments per response: default 20, up to 100. If `pageSize` is also supplied, both values must agree. |
| `maxChars` | Maximum Unicode code points across the `markdown` strings, including labels and indentation: default 12,000, from 512 to 64,000. JSON envelope bytes are additional. This option is rejected for JSON format. |

`meta.modelView.truncated` means more text or records remain. Continue with `meta.pagination.nextCursor`, keeping `format=markdown` and the same fields. A cursor can resume inside a long record; `complete=false` means the record continues. Offsets count Unicode code points in that record's projected JSON representation, before labels and indentation. Read every continuation before ranking or summarizing the entire result set. Reading never creates another collection or charge.

`meta.modelView.characters` counts the returned markdown characters. `tokensEstimate` estimates the serialized fragment array using UTF-8 bytes divided by three, rounded up; it is not model tokenizer usage or an exact context limit. The signed cursor binds the account, task, result snapshot, schema, fields, rendering version and result expiry. To change fields or format, start a new read from the same task without a cursor. Result expiry still applies during continuation.

## Downloads

`POST /v1/tasks/{taskId}/download` with `{"format":"jsonl"}` returns `taskId`, `format`, `expiresAt` and `blocks`. Each block contains `index`, `bytes`, `count`, `sha256` and a signed `url`. URLs last at most five minutes and never beyond the result or credential expiry. Obtain fresh links from the same task if needed.

Authenticated `GET /v1/tasks/{taskId}/download.csv` returns a CSV stream of the same saved records. Spreadsheet formula prefixes are neutralized. An interrupted stream is not a complete file; retry from the saved task.

## History and cancellation

`GET /v1/tasks` returns `items` and `meta.pagination`. Optional filters are `productId`, `status`, `createdAfter` and `createdBefore`; `pageSize` and `cursor` control pagination. Keep filters unchanged while paging.

`POST /v1/tasks/{taskId}/cancel` requests cancellation and requires full-access credentials, an empty JSON body and a canonical command key. A terminal task remains unchanged; inspect this cancel command’s own receipt. Cancellation racing a successful commit may return the already completed task; inspect the returned state.

See [API conventions](/docs/conventions/) and [errors and recovery](/docs/errors/).
