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

> ## Agent Instructions
> Shippo is a multi-carrier shipping API. For agent integrations that execute shipping operations (rates, labels, tracking, address validation, customs), connect the hosted Shippo MCP server at https://mcp.shippo.com (per-user OAuth; setup at /guides/mcp-server). To search and read this documentation from an agent, a docs search MCP is available at https://docs.goshippo.com/mcp. Shipping workflow knowledge (agent skills and a knowledge pack) is published at https://github.com/goshippo/ai. For REST integrations start at /guides/api-quickstart; test mode uses shippo_test_ API keys.

# Create and poll a report run

> Create an async Reporting API run, poll it to completion, and handle SUCCEEDED and FAILED states.

Reporting API is an API to export your Shippo account data as files. Reports run asynchronously: create a run, poll until it reaches `SUCCEEDED`, then download the file. Authenticate with a [Shippo API token](/guides/authentication).

## Create a run

`POST /v2/reporting/runs` returns right away with `status: QUEUED` and a run `id`. Reporting API builds the file in the background.

| Field             | Required | Notes                                                                                                                                        |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `report_type`     | yes      | Versioned key, e.g. `invoice.v1`.                                                                                                            |
| `interval`        | yes      | `{ "start": <epoch_sec>, "end": <epoch_sec> }`. `end` must be greater than `start` (`422` otherwise). Max 92 days (`400 invalid-parameter`). |
| `columns`         | no       | Subset of the report type's `available_columns`, used to pick which columns the file includes. Non-empty if present.                         |
| `output_format`   | no       | `csv`, `csv_gzip` (default), or `parquet`.                                                                                                   |
| `idempotency_key` | no       | Your own string, 1 to 128 characters. See [Safe retries](#safe-retries).                                                                     |

The `interval` is the only way to scope a report. It bounds the run to a UTC date range. Unknown body fields return `422`. The `interval` must fall inside the report type's `data_availability` window. An out-of-window interval returns `404 data-unavailable`. See [Discover report types](/reporting-api/discover-reports) for columns and availability.

```shell theme={null}
curl https://api.goshippo.com/v2/reporting/runs \
  -H "Authorization: ShippoToken <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "report_type": "invoice.v1",
    "interval": { "start": 1704067200, "end": 1706745600 },
    "columns": ["invoice_object_id", "invoice_number", "invoice_total_amount"],
    "output_format": "csv_gzip",
    "idempotency_key": "invoice-jan-2024"
  }'
```

`201 Created`:

```json theme={null}
{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "report_type": "invoice.v1",
  "interval": { "start": 1704067200, "end": 1706745600 },
  "columns": ["invoice_object_id", "invoice_number", "invoice_total_amount"],
  "output_format": "csv_gzip",
  "status": "QUEUED",
  "created": 1704070800
}
```

Keep the `id`. You need it to poll and download.

### Safe retries

Use `idempotency_key` to make creates safe to retry. The key is your own string, so pick one per run you intend to create.

* No key, or a new key you haven't used: `201 Created`, a brand-new run.
* The same key with the same request body: `200 OK`, the existing run handed back. Resend after a network blip and you won't get a duplicate.
* The same key with a different request body: `409` with type segment `idempotency-conflict`. Use a fresh key for a different run.

Without a key, every create makes a new run. There's no automatic deduplication. If create returns a transient `503 service-unavailable`, resend the same request.

## Poll status

There's no webhook. Poll `GET /v2/reporting/runs/{run_id}` until `status` is terminal.

```shell theme={null}
curl https://api.goshippo.com/v2/reporting/runs/d290f1ee-6c54-4b01-90e6-d701748f0851 \
  -H "Authorization: ShippoToken <API_TOKEN>"
```

| Status       | Terminal | Carries                                   |
| ------------ | -------- | ----------------------------------------- |
| `QUEUED`     | no       | Base fields only.                         |
| `PROCESSING` | no       | Base fields only.                         |
| `SUCCEEDED`  | yes      | `completed` + `result` with download URL. |
| `FAILED`     | yes      | `completed` + `error` object.             |

Poll until the run reaches `SUCCEEDED` or `FAILED`.

An unknown run id returns `404 run-not-found`. To list your recent runs, use `GET /v2/reporting/runs` (`limit` defaults to 50, range 1 to 500).

## Handle a run with `status: SUCCEEDED`

```json theme={null}
{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "status": "SUCCEEDED",
  "completed": 1704071460,
  "result": {
    "row_count": 12840,
    "size_bytes": 482113,
    "download": {
      "status": "available",
      "url": "https://<download-host>/<opaque-path>?<signature>",
      "expires": 1704157860
    }
  }
}
```

Download the file with a `GET` request to `result.download.url`; a Shippo `Authorization` header is not required. If `download.status` is `unavailable`, fetch the run again to request a new URL.

<Warning>
  The download URL is time-limited. The `expires` field contains its expiry as a UTC epoch second. If it expires, fetch the run again for a new URL.
</Warning>

## Handle a run with `status: FAILED`

A run with `status: FAILED` returns **HTTP 200** with `status: FAILED`, not an HTTP error. It also carries `completed` and an `error` object. A timeout is a FAILED run, not a separate state.

```json theme={null}
{
  "status": "FAILED",
  "completed": 1704074400,
  "error": { "code": "run_timeout", "message": "The run exceeded its processing time limit." }
}
```

| `error.code`            | Action                                     |
| ----------------------- | ------------------------------------------ |
| `invalid_run_request`   | Fix the request parameters, then resubmit. |
| `run_timeout`           | Transient. Retry with a narrower interval. |
| `run_processing_failed` | Transient. Create a new run.               |
| `run_failed`            | Transient. Create a new run.               |
| `service_unavailable`   | Transient. Create a new run.               |

## HTTP errors

Reporting API errors use `application/problem+json` with `type`, `title`, `status`, and `detail`, except authentication failures, which use `401 application/json` with a `detail` field. Match problem responses on the trailing segment of `type`, since its host can change without changing the error code.

| `type` segment                               | HTTP  | When                                                                      |
| -------------------------------------------- | ----- | ------------------------------------------------------------------------- |
| `invalid-parameter`                          | `400` | A parameter is out of range, e.g. an interval wider than 92 days.         |
| `invalid-column`                             | `400` | A requested column is not in the report type's catalog.                   |
| —                                            | `401` | Missing or invalid API token; handle by HTTP status.                      |
| `report-type-not-found` / `data-unavailable` | `404` | Unknown report type, or an interval outside the data-availability window. |
| `idempotency-conflict`                       | `409` | Reused an `idempotency_key` with a different request body.                |
| `validation-error`                           | `422` | The request body is malformed or has unknown fields.                      |
| `service-unavailable`                        | `503` | Temporary outage. Retry the request.                                      |

## Next steps

<CardGroup cols={2}>
  <Card title="Discover report types" icon="magnifying-glass" href="/reporting-api/discover-reports">
    Read columns and the data-availability window before creating a run.
  </Card>

  <Card title="Reporting API reference" icon="code" href="/api-reference/reporting-api/overview">
    Full request and response schemas for every endpoint.
  </Card>
</CardGroup>


## Related topics

- [Discover available reports](/reporting-api/discover-reports.md)
- [Download report files](/reporting-api/download-artifacts.md)
- [Reporting API Errors and Troubleshooting](/reporting-api/errors.md)
