---
title: "Export records to a file"
method: POST
path: "/api/v1/exports"
tags: ["Exports"]
---

# Export records to a file

`POST /api/v1/exports`

## Request

- Base URL: `https://arcnautical.com`
- URL: `https://arcnautical.com/api/v1/exports`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-ArcNautical-Key`
  - HTTP bearer
  - API key in header `X-ArcNautical-Key`
  - HTTP bearer
  - API key in header `X-ArcNautical-Key`

Start a bulk export and get back an id. The file is built in the background and downloaded from GET /api/v1/exports/{id}/download once status is completed — typically within a minute.

This is the answer to "give me everything we screened in Q3". It is a job rather than a large GET because a year of a real fleet does not fit inside a request timeout, and a response that dies halfway looks like a partial answer.

The row count is measured BEFORE the job is created. If the range holds more than 500,000 rows the request is refused with the real number, rather than producing a file that is silently short — an export missing rows is worse than no export, because everyone downstream treats it as complete.

If you leave filters.created_before out, it is pinned to the moment of the request and returned to you. That makes the export reproducible: without it, a job picked up a minute later would include rows written in between, and the same "Q3" request would produce a different file tomorrow.

An export reserves no quota and screens nothing. It reads records you already hold, and is governed by the same scope that governs reading them one at a time.

Required scope: any one of vessel:screen, cargo:screen, logs:read, whichever governs the resource you name.

## Headers

- `Idempotency-Key` string, required

## Request body

- ExportRequest
  - `resource` 'screenings' | 'cargo_screenings' | 'access_log' | 'voyage_assessments', required: What to export. Each is governed by the same scope that governs reading it one record at a time: screenings by vessel:screen, cargo_screenings by cargo:screen, access_log by logs:read.
  - `format` 'csv' | 'jsonl': csv is a flat summary with a UTF-8 BOM, for a spreadsheet. jsonl is one complete record per line — for screenings that is the full evidence, identical to GET /api/v1/screenings/{id}, including match detail and per-source freshness the CSV cannot carry.
  - `filters` object: A filter that does not apply to the chosen resource is rejected, never ignored — an export that quietly dropped a filter would hand you a wrong file with nothing to show anything went wrong.
    - `created_after` string, date-time: Inclusive lower bound.
    - `created_before` string, date-time: Inclusive upper bound. Defaults to the moment of the request, and is echoed back.
    - `imo` string: screenings, cargo_screenings.
    - `customer_reference` string: screenings, cargo_screenings.
    - `sanctions_status` 'RED' | 'AMBER' | 'INCOMPLETE' | 'GREEN': screenings. "Export everything that came back RED" — the auditor's question, answered inside the artifact whose SHA-256 we publish, rather than by filtering the file afterwards. A value outside this list is a 422, never an empty file: zero rows and "nothing was RED" must not be the same answer.
    - `status` union: access_log: the HTTP status code, e.g. 403 for every refusal. cargo_screenings: the published cargo verdict. Same name, two resources, two vocabularies — a value of the wrong kind is rejected rather than matched against nothing.
      - integer
      - 'CRITICAL' | 'WARNING' | 'OK'
    - `outcome` 'success' | 'failure': access_log.
    - `method` string: access_log.
    - `endpoint` string: access_log. Templated path, e.g. /api/v1/screenings/{id}.
    - `key_id` string, uuid: access_log.

## Response `202`

Export accepted and queued.

- Export
  - `id` string, uuid, required
  - `resource` 'screenings' | 'cargo_screenings' | 'access_log' | 'voyage_assessments', required: What to export. Each is governed by the same scope that governs reading it one record at a time: screenings by vessel:screen, cargo_screenings by cargo:screen, access_log by logs:read.
  - `format` 'csv' | 'jsonl', required
  - `status` 'queued' | 'running' | 'completed' | 'failed', required
  - `filters` object, required: The filters as applied, including the pinned created_before.
  - `estimated_row_count` integer, nullable: Rows matched when the export was accepted.
  - `row_count` integer, nullable: Rows actually written. Null until completed.
  - `byte_size` integer, nullable
  - `checksum_sha256` string, nullable: SHA-256 of the file body, hex.
  - `download_url` string: Present only when status is completed.
  - `error` ApiErrorEnvelope
    - `code` 'missing_idempotency_key' | 'invalid_idempotency_key' | 'invalid_json' | 'unauthorized' | 'invalid_api_key' | 'quota_exhausted' | 'insufficient_scope' | 'entitlement_inactive' | 'ip_not_allowed' | 'monitoring_unavailable_in_test' | 'simulation_unavailable_in_live' | 'not_found' | 'unsupported_api_version' | 'method_not_allowed' | 'unsupported_media_type' | 'idempotency_conflict' | 'idempotency_in_progress' | 'export_not_ready' | 'delivery_in_flight' | 'endpoint_disabled' | 'endpoint_deleted' | 'monitor_limit_exceeded' | 'vessel_monitor_limit_exceeded' | 'vessel_monitor_exists' | 'export_unavailable' | 'validation_failed' | 'route_not_found' | 'export_too_large' | 'webhook_url_blocked' | 'webhook_url_unresolvable' | 'rate_limit_exceeded' | 'concurrency_limit' | 'service_concurrency_limit' | 'export_concurrency_limit' | 'internal_error' | 'assessment_failed' | 'screening_failed' | 'export_failed' | 'export_row_count_grew' | 'api_paused' | 'assessment_timeout' | 'screening_timeout', required: Stable machine code. Branch on this, never on the message text, which we reword. Generated from the list the server throws from rather than restated here, because a hand-kept copy of a server-side vocabulary has drifted on us before. The HTTP status does NOT tell you whether to retry: 409 idempotency_in_progress resolves by waiting while 409 idempotency_conflict never resolves, and 500 export_row_count_grew fails identically on every retry. Retryability per code: - `missing_idempotency_key` (400) — not retryable. A POST arrived with no Idempotency-Key header. - `invalid_idempotency_key` (400) — not retryable. The Idempotency-Key was present but unusable (empty or over-long). - `invalid_json` (400) — not retryable. The body did not parse as JSON. - `unauthorized` (401) — not retryable. No credential was supplied. - `invalid_api_key` (401) — not retryable. The key is unknown, revoked, expired, or its prefix does not match the environment. - `quota_exhausted` (402) — not retryable. The allowance for this resource is spent: the month's (GET /api/v1/usage carries the reset date), or a one-time total (live screenings on an account opened from 2026-10-01, or a pilot), which does not reset. - `insufficient_scope` (403) — not retryable. The key does not hold the scope this endpoint requires. - `entitlement_inactive` (403) — not retryable. The account has no active entitlement. - `ip_not_allowed` (403) — not retryable. The key is IP-restricted and this source address is not on its list. - `monitoring_unavailable_in_test` (403) — not retryable. Vessel and corridor monitors are not available on a test key. - `simulation_unavailable_in_live` (403) — not retryable. Simulated monitor events are available only on a test key. A synthetic risk change on a live monitor would enter your compliance record as a real finding about a real vessel. - `not_found` (404) — not retryable. No such resource, or it is not yours. A malformed id answers this way too, deliberately. - `unsupported_api_version` (404) — not retryable. The path names an /api/vN this service does not publish. Answered by the edge, so it is the one code that carries no rate-limit headers. NEVER retry: our own guidance says an HTML body means a network layer answered and should be retried, and this exists so that advice is not applied to a permanent 404. - `method_not_allowed` (405) — not retryable. The path exists but not with this verb. The Allow header names the verbs that do work. - `unsupported_media_type` (415) — not retryable. Content-Type declared something other than application/json. Omitting the header is accepted; declaring the wrong one is not. - `idempotency_conflict` (409) — not retryable. This Idempotency-Key was already used with a different body. Never resolves — use a new key. - `idempotency_in_progress` (409) — RETRYABLE. The original request with this key is still running, and this retry waited for it (up to 30s) without it finishing. Retry with the same key: the next retry waits again and returns the original result the moment it lands, flagged X-ArcNautical-Idempotent-Joined: true. Carries Retry-After. - `export_not_ready` (409) — RETRYABLE. The export job has not finished. Poll it; carries Retry-After. - `delivery_in_flight` (409) — RETRYABLE. That webhook delivery is being attempted right now. - `endpoint_disabled` (409) — not retryable. The webhook endpoint is disabled. Re-enable it first. - `endpoint_deleted` (409) — not retryable. The webhook endpoint no longer exists. - `monitor_limit_exceeded` (409) — not retryable. Corridor monitor ceiling reached. Delete one first. - `vessel_monitor_limit_exceeded` (409) — not retryable. Vessel monitor ceiling reached. Delete one first. - `vessel_monitor_exists` (409) — not retryable. A monitor already watches this hull. - `export_unavailable` (410) — not retryable. The export file has aged out. The records behind it are unaffected — create a new one. - `validation_failed` (422) — not retryable. One or more fields are invalid; field_violations names each one. - `route_not_found` (422) — not retryable. Both ports are known but no sea route connects them. - `export_too_large` (422) — not retryable. The requested range exceeds the per-export row cap. Narrow the filters and export in parts. - `webhook_url_blocked` (422) — not retryable. The URL resolves to a private, loopback or link-local address. - `webhook_url_unresolvable` (422) — not retryable. The URL host does not resolve. - `rate_limit_exceeded` (429) — RETRYABLE. Over the per-key request rate. Shed RATE; back off to X-RateLimit-Reset. - `concurrency_limit` (429) — RETRYABLE. Over your own in-flight ceiling. Shed CONCURRENCY, or use the batch endpoint. Retry-After carries a live estimate, not a fixed number. - `service_concurrency_limit` (429) — RETRYABLE. The shared pool is full and your account is within its allowance. A graded screen QUEUES for that pool first and is served round-robin between accounts, so this means the queue itself ran out of time — you were not stuck behind one customer's batch. Reducing your fan-out will NOT help; retry unchanged after Retry-After. include_vetting: false skips this queue entirely, and /api/v1/screening-batches is not subject to it at all. - `export_concurrency_limit` (429) — RETRYABLE. Too many exports building at once. Wait for one to finish. - `internal_error` (500) — RETRYABLE. An unhandled failure. Retry with backoff; quote request_id if it persists. - `assessment_failed` (500) — RETRYABLE. Voyage scoring failed to produce a result. - `screening_failed` (500) — RETRYABLE. The screening engine failed to produce a verdict. No unit is charged. - `export_failed` (500) — RETRYABLE. The export job failed while building. - `export_row_count_grew` (500) — not retryable. The matching rows grew past the cap while the file was being written, so it was abandoned rather than shipped short. Narrow the filters — an identical retry fails identically. - `api_paused` (503) — RETRYABLE. The API is paused for a short while (an operator lever against abuse, rarely pulled). Nothing ran and nothing was charged. Retry the same request after Retry-After. - `assessment_timeout` (504) — RETRYABLE. Scoring exceeded its deadline. Retry; a warm route usually succeeds. - `screening_timeout` (504) — RETRYABLE. The screening exceeded its time budget (screeningComputeBudgetMs) and was abandoned before it could be stored. No unit is charged, and the Idempotency-Key is RELEASED — so retry with the SAME key; it is a clean attempt, not a replay. Deliberately raised below the socket timeout at the edge, so this arrives as JSON with a request_id rather than as a gateway 504 we never see.
    - `message` string, required
    - `request_id` string, required
    - `field_violations` FieldViolation[]
      - `field` string, required
      - `message` string, required
  - `download_count` integer
  - `downloaded_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `completed_at` string, date-time, nullable
  - `expires_at` string, date-time, required: When the FILE is deleted. The records it was built from are unaffected.

## Other responses

- `400`: Validation error. `code` is one of: - `missing_idempotency_key` — A POST arrived with no Idempotency-Key header. - `invalid_idempotency_key` — The Idempotency-Key was present but unusable (empty or over-long). - `invalid_json` — The body did not parse as JSON.
- `401`: Missing or invalid API key. `code` is one of: - `unauthorized` — No credential was supplied. - `invalid_api_key` — The key is unknown, revoked, expired, or its prefix does not match the environment.
- `402`: The allowance for this resource is used up: the month's, or a one-time total (live screenings on an account opened from 2026-10-01, or a pilot), which does not reset (then there is no reset instant and no Retry-After). The refused request is NOT metered. message states used/limit and the reset instant when there is one; a replay under an Idempotency-Key already answered is still free and is not metered, reads are never metered, and a test-environment key has its own allowance. `code` is one of: - `quota_exhausted` — The allowance for this resource is spent: the month's (GET /api/v1/usage carries the reset date), or a one-time total (live screenings on an account opened from 2026-10-01, or a pilot), which does not reset.
- `403`: The key is not entitled to this environment or scope. `code` is one of: - `insufficient_scope` — The key does not hold the scope this endpoint requires. - `entitlement_inactive` — The account has no active entitlement. - `ip_not_allowed` — The key is IP-restricted and this source address is not on its list. - `monitoring_unavailable_in_test` — Vessel and corridor monitors are not available on a test key. - `simulation_unavailable_in_live` — Simulated monitor events are available only on a test key. A synthetic risk change on a live monitor would enter your compliance record as a real finding about a real vessel.
- `404`: Resource not found. `code` is one of: - `not_found` — No such resource, or it is not yours. A malformed id answers this way too, deliberately. - `unsupported_api_version` — The path names an /api/vN this service does not publish. Answered by the edge, so it is the one code that carries no rate-limit headers. NEVER retry: our own guidance says an HTML body means a network layer answered and should be retried, and this exists so that advice is not applied to a permanent 404.
- `405`: The path exists but not with this method. The Allow header names the methods that do. `code` is one of: - `method_not_allowed` — The path exists but not with this verb. The Allow header names the verbs that do work.
- `409`: Idempotency conflict. `code` is one of: - `idempotency_conflict` — This Idempotency-Key was already used with a different body. Never resolves — use a new key. - `idempotency_in_progress` (retryable) — The original request with this key is still running, and this retry waited for it (up to 30s) without it finishing. Retry with the same key: the next retry waits again and returns the original result the moment it lands, flagged X-ArcNautical-Idempotent-Joined: true. Carries Retry-After. - `export_not_ready` (retryable) — The export job has not finished. Poll it; carries Retry-After. - `delivery_in_flight` (retryable) — That webhook delivery is being attempted right now. - `endpoint_disabled` — The webhook endpoint is disabled. Re-enable it first. - `endpoint_deleted` — The webhook endpoint no longer exists. - `monitor_limit_exceeded` — Corridor monitor ceiling reached. Delete one first. - `vessel_monitor_limit_exceeded` — Vessel monitor ceiling reached. Delete one first. - `vessel_monitor_exists` — A monitor already watches this hull.
- `415`: Content-Type describes a body that is not JSON (form-encoded, multipart, XML). application/json, text/plain and an absent header are all accepted. `code` is one of: - `unsupported_media_type` — Content-Type declared something other than application/json. Omitting the header is accepted; declaring the wrong one is not.
- `422`: Request or route validation failed. Also returned for a body field this endpoint does not read — field_violations names it and lists the accepted keys. `code` is one of: - `validation_failed` — One or more fields are invalid; field_violations names each one. - `route_not_found` — Both ports are known but no sea route connects them. - `export_too_large` — The requested range exceeds the per-export row cap. Narrow the filters and export in parts. - `webhook_url_blocked` — The URL resolves to a private, loopback or link-local address. - `webhook_url_unresolvable` — The URL host does not resolve.
- `429`: Rate limit exceeded. `code` is one of: - `rate_limit_exceeded` (retryable) — Over the per-key request rate. Shed RATE; back off to X-RateLimit-Reset. - `concurrency_limit` (retryable) — Over your own in-flight ceiling. Shed CONCURRENCY, or use the batch endpoint. Retry-After carries a live estimate, not a fixed number. - `service_concurrency_limit` (retryable) — The shared pool is full and your account is within its allowance. A graded screen QUEUES for that pool first and is served round-robin between accounts, so this means the queue itself ran out of time — you were not stuck behind one customer's batch. Reducing your fan-out will NOT help; retry unchanged after Retry-After. include_vetting: false skips this queue entirely, and /api/v1/screening-batches is not subject to it at all. - `export_concurrency_limit` (retryable) — Too many exports building at once. Wait for one to finish.
- `500`: Internal or upstream error. `code` is one of: - `internal_error` (retryable) — An unhandled failure. Retry with backoff; quote request_id if it persists. - `assessment_failed` (retryable) — Voyage scoring failed to produce a result. - `screening_failed` (retryable) — The screening engine failed to produce a verdict. No unit is charged. - `export_failed` (retryable) — The export job failed while building. - `export_row_count_grew` — The matching rows grew past the cap while the file was being written, so it was abandoned rather than shipped short. Narrow the filters — an identical retry fails identically.
- `503`: Service temporarily unavailable. `code` is one of: - `api_paused` (retryable) — The API is paused for a short while (an operator lever against abuse, rarely pulled). Nothing ran and nothing was charged. Retry the same request after Retry-After.
- `504`: The request exceeded its server-side time budget and was abandoned. Raised by us, below the socket timeout at the edge, so it arrives as JSON with a request_id. `code` is one of: - `assessment_timeout` (retryable) — Scoring exceeded its deadline. Retry; a warm route usually succeeds. - `screening_timeout` (retryable) — The screening exceeded its time budget (screeningComputeBudgetMs) and was abandoned before it could be stored. No unit is charged, and the Idempotency-Key is RELEASED — so retry with the SAME key; it is a clean attempt, not a replay. Deliberately raised below the socket timeout at the edge, so this arrives as JSON with a request_id rather than as a gateway 504 we never see.

---

[API](https://skmtc.dev/arcnautical/apis/arcnautical-api.md) · [All operations](https://skmtc.dev/arcnautical/apis/arcnautical-api/llms.txt) · [OpenAPI document](https://skmtc.dev/arcnautical/apis/arcnautical-api/revisions/40cd2659cdd7?raw)
