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

# Import outcomes

> What happened to every row of a bulk upload — the four per-row outcomes, the provider's own reject text, the counters they roll up into, and the terminal state a batch settles at

A bulk upload is not one operation with one result. Your platform provider answers **per row**: it takes some records into the list and refuses others, and it can do both inside a single upload call. GetDialed records that answer on each row, keeps the provider's own words for the rows it refused, and rolls the whole thing up into a batch state that says honestly what happened.

## The outcome lives on the row

Every uploaded record is one [job](/concepts/batches-and-executions#jobs), and the job's final status **is** its outcome. The batch carries the rollup — the counts and the terminal state — never a per-record array.

That split is why you read outcomes with the jobs feed: `GET /flows/jobs?batch_id=<batch_id>&status=rejected` is the list of rows the provider refused, and the same filter with any other status answers the same question for that state.

## The four outcomes

| Status        | Meaning                                                                                                                                |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `accepted`    | The provider took this row into the list. This is the success case, and on a healthy import it is nearly every row.                    |
| `rejected`    | The provider refused this row and said why. The row was **not** added, and the reason is kept verbatim.                                |
| `unprocessed` | The row was never sent. Its batch was cancelled before this row reached the provider — so nothing about this record left the platform. |
| `unresolved`  | The outcome could not be determined, and never will be.                                                                                |

All four are terminal: the row will not change again, and its retention clock starts at that moment like any other finished record.

<Note>
  These four join the statuses a row passes through on its way there — `staged`, `queued`, and the non-terminal `needs_reconcile` hold. See the [full job status table](/concepts/batches-and-executions#job-status-lifecycle) for the whole lifecycle in one place.
</Note>

### `unresolved`, stated plainly

`unresolved` means: **we do not know what happened to this row.** The upload handle was lost, or the provider's answer never became conclusive, or the result did not arrive inside the window we are willing to wait (roughly six hours). One of those is true, and none of them tells us whether the record reached your list.

Three things follow, and they are all deliberate:

* **The row was not retried.** Re-sending a record whose fate is unknown risks contacting the same person twice, so nothing in the platform does it automatically.
* **The row will not be retried later either.** `unresolved` is terminal precisely so that an unknowable row cannot hold a batch open forever.
* **Re-driving it is yours to decide.** [Export the unresolved rows](/guides/export-failed-rows) and re-upload them as a new record set if you judge a possible duplicate contact acceptable. That is a real risk, not a formality — the whole reason these rows are quarantined is that we cannot rule it out.

<Warning>
  Do not confuse `unresolved` with the non-terminal `needs_reconcile` hold. `needs_reconcile` means "a send is still being chased down"; those rows are held and can be released with `POST /flows/batches/{batch_id}/requeue`. `unresolved` is where a row lands when that chase ends without an answer — requeue does not apply to it.
</Warning>

## Why a row was rejected

A rejected row carries **two** descriptions of the same refusal, because neither one alone is enough:

| Column            | What it is                                                                                                                                                              |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trouble_message` | The provider's own text, stored **exactly** as it was returned — never trimmed, never reworded, never re-interpreted. This is what a person reads when fixing the file. |
| `reject_class`    | A normalized category, so you can filter, group and count reject reasons without parsing prose.                                                                         |

The provider's text is the truth about *this* row and occasionally describes the rule it thinks was broken imprecisely; the class is what stays stable enough to build on. Keeping both means neither has to do the other's job.

Classes you can see today:

| `reject_class`    | Typically means                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| `parse_error`     | The provider could not read the row — a malformed value in one of the mapped columns. By far the most common. |
| `duplicate_key`   | The row matched an existing record on the upload's key fields.                                                |
| `missing_key`     | Every key field on the row was empty.                                                                         |
| `no_match`        | No existing contact matched, on an upload that required one.                                                  |
| `matched_one`     | Exactly one existing contact matched.                                                                         |
| `ambiguous_match` | Several contacts matched, so the target was ambiguous.                                                        |
| `provider_error`  | The provider hit an internal error handling the row.                                                          |
| `unknown`         | A reason this platform does not recognise yet.                                                                |

<Note>
  The class list is **extensible on purpose**. If your provider starts returning a reason we have not classified, that row arrives as `unknown` with its `trouble_message` intact — an unfamiliar reason is never guessed at and never causes the rest of the import's results to be dropped.
</Note>

Both columns are delivered in the [failed-row CSV export](/guides/export-failed-rows), which is the surface built for reading them: the export puts them next to the original record so the offending column is obvious. They are not fields on the `GET /flows/jobs` JSON — a rejection reason is provider prose about a specific contact, and the export is where that belongs.

## Reading outcomes back

| You want                                  | Ask for                                                                          |
| ----------------------------------------- | -------------------------------------------------------------------------------- |
| The rows a provider refused               | `GET /flows/jobs?batch_id=<batch_id>&status=rejected`                            |
| The rows never sent because you cancelled | `GET /flows/jobs?batch_id=<batch_id>&status=unprocessed`                         |
| The rows whose fate is unknown            | `GET /flows/jobs?batch_id=<batch_id>&status=unresolved`                          |
| A file you can fix and re-upload          | `POST /flows/jobs/export` — see [Export failed rows](/guides/export-failed-rows) |

<Warning>
  **After a large import, the default jobs feed is mostly `accepted` rows.** `GET /flows/jobs` returns every job in the account newest-first, so a freshly finished million-row upload dominates the first page and everything else is pages behind it.

  Filter rather than page: add `status=` for the outcome you care about, `batch_id=` for one run, or `record_set_id=` for one upload's own view. Scoping by `record_set_id` also **includes that upload's `staged` rows**, which the account-wide feed hides by default.
</Warning>

## The batch counters

A batch keeps a running count of where its rows stand. The counts are maintained as rows move — never recomputed by scanning your records — so they stay cheap at any volume.

| Counter       | Counts rows that are                                            |
| ------------- | --------------------------------------------------------------- |
| `total`       | Bound to this batch. The whole.                                 |
| `queued`      | Waiting to be sent.                                             |
| `dispatched`  | Handed to the provider, result not back yet.                    |
| `accepted`    | Taken into the list.                                            |
| `rejected`    | Refused, with a reason.                                         |
| `unprocessed` | Never sent, because the batch was cancelled.                    |
| `unresolved`  | Outcome undeterminable.                                         |
| `parked`      | Held pending an explicit re-drive — the `needs_reconcile` rows. |

They always add up:

```
total == queued + dispatched + accepted + rejected + unprocessed + unresolved + parked
```

That invariant is the point of the design. Every row is in exactly one bucket at every instant, so a row can never quietly disappear between "sent" and "answered" — if a count looks wrong, the arithmetic says so immediately rather than after a reconciliation.

<Note>
  **Where you see them.** The full breakdown is delivered with the batch's terminal frame on `GET /flows/batches/{batch_id}/stream` — one aggregate event carrying the final status and every counter (see [Execution streaming](/concepts/execution-streaming)). While a batch is still running, list its rows by status to see what is in each state.
</Note>

## Batch terminal states

The counters decide where the batch lands. There is exactly one rule, applied the same way whether the batch finished on its own or you stopped it:

| Terminal state          | Reached when                                                                                                         |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `completed`             | At least one row accepted, and nothing rejected or unresolved. A clean import.                                       |
| `completed_with_errors` | At least one row accepted, **and** some rows rejected or unresolved. Mostly worked.                                  |
| `failed`                | **Zero** rows accepted, with rejections or unresolved rows.                                                          |
| `cancelled`             | You cancelled it. Cancel wins over everything else — a cancelled batch reports `cancelled`, whatever its counts say. |

<Warning>
  **Zero accepted is `failed`, not `completed_with_errors`.** A batch where nothing at all was taken into the list is almost never a data-quality problem spread across your records — it is one structural mistake: the wrong list, a bad column mapping, a file whose header did not match. That has to read loudly on a dashboard and in a stream, so it gets the loud status. `completed_with_errors` keeps its plain meaning: this mostly worked, and here are the rows that did not.
</Warning>

`completed_with_errors` is a **terminal** state like any other: the batch is finished, its stream closes, and nothing further will be dispatched from it. It is distinct from `partially_failed`, which describes the batch's *executions* rather than its provider import — the two live at different layers and stay separately readable.

## What cancelling does to the counts

Cancelling a batch — `POST /flows/batches/{batch_id}/cancel`, available to any key for your own organization — stops all future sends and nothing else:

* **Further sends stop the moment the call returns.** Nothing still waiting in the queue is handed to the provider after that point, whatever its size.
* Every row **not yet sent** settles as `unprocessed`. None is left in limbo. On a large batch the counts land a few seconds behind the response — settling a million rows is real work, and it happens in the background rather than holding your request open — so read `counters.unprocessed` a moment later, not in the same breath.
* Records **already handed to the provider are not recalled.** Their results are still collected and written back exactly as they would have been, so `accepted` and `rejected` keep filling in after the cancel.
* When the dust settles, the counters tell you precisely what reached the provider before you pulled the brake — which is the number you actually need after cancelling a bad upload.

Cancel is a one-way door for future sends: held work on a cancelled batch cannot be requeued afterwards.

## When outcomes arrive

Results are collected by **asking the provider**, on a schedule that respects your platform's rate budget. That polling is the authoritative path and it always runs.

Where a provider can also *push* a notice that an import has finished, that push is only a hint that it is worth asking now — it shortens the wait and never carries the outcome itself. A notice that is late, duplicated or never sent changes nothing about the result you get; it only changes how quickly you get it.

If an import never becomes conclusive within roughly six hours, its rows settle as `unresolved` rather than being chased indefinitely.

## One event per batch, never one per row

A finished bulk batch emits **one** aggregate event carrying its terminal status and its counters. It does not emit an event per record — a million-row upload would otherwise be a million frames on a stream a person is watching, which is a denial of service dressed up as observability.

Per-row detail is a query (`GET /flows/jobs?batch_id=...&status=...`) or a [file](/guides/export-failed-rows), not a stream.

## Next steps

<CardGroup cols={2}>
  <Card title="Export failed rows" icon="file-csv" href="/guides/export-failed-rows">
    Fix and resubmit: list the rejected rows, export them, correct the file, re-upload.
  </Card>

  <Card title="Bulk ingestion" icon="upload" href="/guides/bulk-ingestion">
    The upload path these outcomes come from, end to end.
  </Card>

  <Card title="Batches, jobs, and executions" icon="diagram-project" href="/concepts/batches-and-executions">
    Every status in the chain, and where outcomes sit in it.
  </Card>

  <Card title="Dispatch pacing" icon="gauge-high" href="/concepts/dispatch-pacing">
    Why a large upload is delivered in slices, and what `needs_reconcile` means.
  </Card>
</CardGroup>
