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

# Source import API

> Send spreadsheet contents as JSON through your existing source adapters, on cloud or on-premises

Send rows from your ERP or another system directly to Azalt, without uploading an Excel file. Select an existing source adapter by ID and submit the sheet names, column names, and values it expects. Azalt uses the same mapping, validation, duplicate handling, calculations, approval workflow, and source-row history as imports on the `/data` page.

This guide covers the source adapter REST API, not the MCP server or direct activity creation.

## Before you start

Your installation must include the JSON source import API release and its database migration. Check your installation's `/api/v1/openapi.json` for `/source-adapters` and `/source-imports`; older releases do not expose these endpoints. Publishing this guide does not upgrade an installation. See [On-premises and Helm split deployment](#on-premises-and-helm-split-deployment) below.

You need:

* An active source adapter with a JSON API input contract configured by an organization owner.
* An API key from **Settings → API keys** (`/settings/api-keys`) in the intended organization. Use a dedicated integration user and store its key in a secret manager, not source control or shared logs.
* A **Collector**, **Approver**, **Manager**, or **Owner** role to submit and execute imports, with access to the target sites. **Viewer** access is sufficient for discovery and permitted run reads.
* `curl` and `jq` to follow the examples.

Requests use `Authorization: Bearer YOUR_API_KEY`. The key determines the organization and user; changing the organization in the browser does not retarget the key. Role, active membership, and site access are checked against current permissions.

Set `AZALT_API_KEY` securely in your environment. Set the base URL to your own installation, without a trailing slash or `/api/v1` suffix:

```bash theme={null}
export AZALT_API_URL="https://azalt.example.com"
```

For Azalt Cloud, use `https://app.azalt.co` once the API release is deployed there. On-premises integrations use their own Azalt hostname.

## 1. Enable an adapter

An organization owner opens **Customization → Source adapters**, edits the adapter, and fills in **JSON API input contract**. Keep the adapter active and save the configuration.

The following uses fictional invoice values and is a template, not a universal invoice format. The contract must match the sheet and column names consumed by your adapter's script. Adding a contract validates and describes input; it does not change the script or its destination mappings.

```json theme={null}
{
  "enabled": true,
  "context": { "yearRequired": true, "siteRequired": false },
  "sheets": [
    {
      "name": "Sheet1",
      "required": true,
      "additionalColumns": false,
      "fields": [
        {
          "name": "FATURA ID",
          "type": "string",
          "required": true,
          "example": "INV-2026-0001"
        },
        {
          "name": "TUTAR",
          "type": "number",
          "required": true,
          "example": -125.5
        },
        {
          "name": "BİRİM",
          "type": "string",
          "required": true,
          "example": "Litre"
        }
      ]
    }
  ]
}
```

Existing adapters are not automatically enabled for JSON imports. Removing the contract or setting `enabled` to `false` stops new JSON imports without disabling file uploads. Previously saved previews retain their mapped drafts.

### Accepted data types

| Type       | JSON example                  | Meaning                                                            |
| ---------- | ----------------------------- | ------------------------------------------------------------------ |
| `string`   | `"00001234"`                  | Text; use for identifiers to preserve leading zeros and precision. |
| `number`   | `-125.5`                      | A finite JSON number, not a quoted numeric string.                 |
| `integer`  | `42`                          | A whole number within JavaScript's safe integer range.             |
| `boolean`  | `true`                        | A JSON boolean, not `"true"` or `1`.                               |
| `date`     | `"2026-09-08"`                | A valid date in `YYYY-MM-DD` format.                               |
| `datetime` | `"2026-09-08T10:30:00+03:00"` | An ISO 8601 date-time with a timezone (`Z` or an offset).          |

Fields can have `description`, `example`, `required` (default `false`), and `nullable` (default `false`). `required` means the key must be present, not that a string must be non-empty. Explicit `null` requires `nullable: true`; omitted optional fields are not filled in. Dates remain strings, and values are not automatically converted between types. Declare a localized amount such as `"1.234,50"` as `string` only if your adapter parses that format.

Sheet and column names are exact and case-sensitive. Unknown sheets are rejected. Unknown columns are rejected unless the sheet sets `additionalColumns: true`. A required sheet must contain at least one row. Names cannot have surrounding whitespace or reserved object keys. Cells cannot contain nested objects or arrays, NUL characters, or invalid Unicode.

Source values, including whitespace, zero, booleans, nulls, and negative numbers, are preserved. If destination amounts must be positive, implement that transformation in the adapter; the API does not automatically take absolute values. The saved source still shows the submitted negative value.

## 2. Discover adapters and their contracts

```bash theme={null}
curl --fail-with-body \
  "$AZALT_API_URL/api/v1/source-adapters?limit=50&offset=0" \
  -H "Authorization: Bearer $AZALT_API_KEY"
```

The response is an array containing each adapter's `id`, `name`, `description`, `isActive`, `apiReady`, and `adapterVersion`. Choose an adapter with `apiReady: true`. Increase `offset` to retrieve another page; `limit` defaults to 50 and cannot exceed 100.

Replace the ID below with the selected adapter's ID:

```bash theme={null}
export AZALT_ADAPTER_ID="REPLACE_WITH_ADAPTER_ID"

curl --fail-with-body \
  "$AZALT_API_URL/api/v1/source-adapters/$AZALT_ADAPTER_ID" \
  -H "Authorization: Bearer $AZALT_API_KEY" \
  --output adapter.json

jq '{id, apiReady, adapterVersion, inputContract, limits, exampleRequest}' adapter.json
```

The detail endpoint returns the accepted sheets, columns, types, limits, and an example request, but not the adapter's script. Copy its example into a working request file:

```bash theme={null}
jq -e 'if .apiReady == true and .exampleRequest != null then .exampleRequest else error("Adapter is not API-ready") end' \
  adapter.json > submission.json
```

Edit `submission.json` before submitting: replace example rows with your real data and set the correct `context.year` and, when required, `context.siteId`. Example values do not guarantee a valid business mapping. Years must be integers from 1900 through 2200; the site ID must be accessible to the integration user.

The request structure looks like this. `ADAPTER_ID` and `COPY_VERSION_FROM_ADAPTER_RESPONSE` are placeholders: use the actual ID and the exact 64-character `adapterVersion` returned by the endpoint. The generated `exampleRequest` already includes them.

```json theme={null}
{
  "sourceAdapterDefinitionId": "ADAPTER_ID",
  "adapterVersion": "COPY_VERSION_FROM_ADAPTER_RESPONSE",
  "mode": "preview",
  "context": { "year": 2026 },
  "sheets": [
    {
      "name": "Sheet1",
      "rows": [
        { "FATURA ID": "INV-2026-0001", "TUTAR": -125.5, "BİRİM": "Litre" }
      ]
    }
  ]
}
```

## 3. Submit a preview

Assign one stable idempotency key to each logical batch. Keep both the key and request body for retries; do not generate a new key after a timeout.

```bash theme={null}
export AZALT_BATCH_KEY="erp-invoices-2026-09-08-batch-001"

curl --fail-with-body "$AZALT_API_URL/api/v1/source-imports" \
  -H "Authorization: Bearer $AZALT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $AZALT_BATCH_KEY" \
  --data-binary @submission.json \
  --output preview.json

jq '{run, counts, execution, replayed}' preview.json
export AZALT_RUN_ID="$(jq -er '.run.id' preview.json)"
```

`preview` is the default mode. It saves source rows and mapped drafts but does not write destination records. Each sheet's first data row has `rowIndex: 2`, reserving row 1 for spreadsheet-style headers.

Read the row-level results and check whether execution requires additional confirmation:

```bash theme={null}
curl --fail-with-body \
  "$AZALT_API_URL/api/v1/source-imports/$AZALT_RUN_ID/rows?limit=100&offset=0" \
  -H "Authorization: Bearer $AZALT_API_KEY"

curl --fail-with-body \
  "$AZALT_API_URL/api/v1/source-imports/$AZALT_RUN_ID/readiness" \
  -H "Authorization: Bearer $AZALT_API_KEY"
```

The rows response contains `rows` and `total`. Each row includes `sheetName`, `rowIndex`, `rawRow`, `status`, `warnings`, and `error`. Pagination defaults to 100 rows and is limited to 200. Add `status=failed` to retrieve only failures; `total` then counts only matching rows.

## 4. Execute the saved preview

After reviewing the mapped results and readiness, execute the same run:

```bash theme={null}
curl --fail-with-body \
  "$AZALT_API_URL/api/v1/source-imports/$AZALT_RUN_ID/execute" \
  -H "Authorization: Bearer $AZALT_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{}' \
  --output execution.json

jq '{run, counts, execution}' execution.json
```

If a destination form has not been opened for a required site and year, execution is blocked pending confirmation. The response keeps the run ID and returns `execution.blocked: true` and `execution.readiness.missingFormSites`. The readiness endpoint also lists the affected forms, sites, years, and source rows.

Only after reviewing and approving those missing form deployments, repeat the execute request with this body:

```json theme={null}
{ "allowMissingFormSiteCreation": true }
```

For an established automated integration, `mode: "execute"` on the initial submission previews and executes in one request. Execution is synchronous, not a background queue job. Missing form deployments still require confirmation unless the initial body explicitly sets `allowMissingFormSiteCreation: true`.

### Interpreting the result

An HTTP 200, or even `run.status: "completed"`, does not guarantee that every row imported. Valid drafts can execute while other rows fail. Always inspect `execution.blocked`, the row counts, and individual warnings/errors.

| Response field                     | What to check                                                               |
| ---------------------------------- | --------------------------------------------------------------------------- |
| `run.id`                           | Store this ID with your batch for status checks and safe execution retries. |
| `run.status`                       | Current run state, such as `previewed`, `completed`, or `failed`.           |
| `counts.total`                     | Number of submitted source rows, not destination records.                   |
| `counts.parsed` / `counts.ready`   | Rows parsed or ready for execution.                                         |
| `counts.imported`                  | Rows marked imported by the import engine.                                  |
| `counts.reduced`                   | Rows combined into another row's form-value write; not missing data.        |
| `counts.skipped` / `counts.failed` | Rows that did not import; inspect their results.                            |
| `execution.blocked`                | Execution needs attention, such as missing-form confirmation.               |
| `replayed`                         | An existing request/run was reused rather than creating a new import.       |

One source row can produce multiple drafts. Actual write counts are `run.activityWriteCount` and `run.formValueWriteCount`. Imported values still follow the normal approval and reporting configuration; import completion does not bypass approval or guarantee appearance in every records view.

JSON imports appear in the normal import history with an “API submission” name. Configured source-document links in Records can search and open the saved source rows in-app or full screen. There is no original Excel workbook to download for a JSON-only submission.

## Retries and adapter changes

* `Idempotency-Key` is mandatory for `POST /source-imports`: 1–200 printable ASCII characters, with no spaces. It is scoped to the organization and API-key owner.
* Repeating the same key and body returns the same run with `replayed: true`. Object key order does not matter; sheet and row array order does. Reusing a key with different data, mode, version, or confirmation settings returns **409**.
* To execute a preview, call `/source-imports/{id}/execute`. Do not change the initial request's `mode` under the original key. The execute endpoint does not require an idempotency header and is safe to retry for the same run.
* After a timeout or transient failure, retry the same request/run. Database locks coordinate concurrent retries across replicas and the UI. Execution can have partially committed writes; retrying the same run finishes remaining ready drafts instead of starting another import.
* Different keys create different imports. Cross-batch business deduplication still depends on the adapter's `idempotencyKey` and duplicate strategy. Do not resend successful rows under a new key unless that strategy handles them.
* A script or input-contract change updates `adapterVersion`. New submissions with a stale version return **409**. Fetch the updated contract, review the changes, and prepare a new batch. Existing previews execute their saved drafts without rerunning an edited adapter script.
* If an adapter fails, inspect the stored row errors, correct the input or adapter, then submit a corrected batch with a new key. Repeating the unchanged failed request does not rerun its mapping.
* Deleted runs are unavailable through these endpoints. Permanently deleting a run retains its idempotency reservation; reusing that key returns **409**, not a fresh import.

Run reads and execution are limited to JSON imports created by the **same API-key owner in the same organization**, even for managers or administrators. Rotating a key for the same user preserves access. Revoking membership or site access can prevent access to an existing run. These endpoints do not expose other users' runs or older file-upload imports; authorized staff can continue using UI import history.

## Endpoint reference

All paths below are relative to `/api/v1` on your installation. Its `/api/v1/openapi.json` provides the deployed request and response schemas.

| Method | Path                             | Purpose                                                                |
| ------ | -------------------------------- | ---------------------------------------------------------------------- |
| GET    | `/source-adapters`               | List adapters and API readiness; `limit` and `offset` pagination.      |
| GET    | `/source-adapters/{id}`          | Get the input contract, version, limits, and example request.          |
| POST   | `/source-imports`                | Save a preview or preview and execute; requires `Idempotency-Key`.     |
| GET    | `/source-imports/{id}`           | Read the current run, version, and row counts.                         |
| GET    | `/source-imports/{id}/rows`      | Read source rows and errors; optional `status`, `limit`, and `offset`. |
| GET    | `/source-imports/{id}/readiness` | Review missing form/site/year deployments.                             |
| POST   | `/source-imports/{id}/execute`   | Execute a saved preview, optionally confirming missing deployments.    |

## Limits and errors

Each request supports at most **10 MiB of JSON, 10,000 total rows, 50 sheets, 200 columns per row, and 16,000 characters per string cell**. Split larger datasets into separate batches with distinct keys. Your ingress or hosting provider may enforce a lower body size or timeout limit.

| HTTP status | Action                                                                                               |
| ----------- | ---------------------------------------------------------------------------------------------------- |
| 400         | Check the JSON structure, contract field types, required context, and idempotency key.               |
| 401         | Supply a valid Bearer API key belonging to an active organization member.                            |
| 403         | Check the user's role and access to every target site.                                               |
| 404         | Check the adapter/run ID, organization, run owner, deletion state, and deployed API version.         |
| 409         | Check for a stale adapter version, changed body under the same key, or a deleted run's reserved key. |
| 412         | Activate the adapter and configure an enabled JSON API input contract.                               |
| 413         | Reduce the request size to fit the API and ingress limits.                                           |
| 415         | Send `Content-Type: application/json`.                                                               |

Business validation failures can be returned as row results rather than HTTP errors. Always inspect the response body before treating a batch as successful.

## On-premises and Helm split deployment

The API works in both the combined Next.js application and the standalone backend used by Helm split installations. Use your normal frontend hostname, or a backend hostname if your operator intentionally exposes it.

Before enabling the integration, the deployment operator should:

1. Back up the intended database and apply the release's additive migration, `20260908000000_add_source_import_api_requests.ts`, using the normal Helm migration job/process. It creates the request-deduplication table.
2. Deploy matching updated frontend and backend images for a split installation. Upgrading only the frontend does not add the backend endpoints.
3. Preserve `/api/v1` routing and the `Authorization`, `Content-Type`, and `Idempotency-Key` headers at the ingress/proxy. Do not cache authenticated responses.
4. Check request-body and synchronous-request timeout limits, then verify the deployed OpenAPI document and run a small reviewed preview before execution.

No S3, MinIO, original-file upload, new storage environment variables, or background queue is required for JSON imports or their saved source-data preview. Existing file imports keep their own storage requirements.

If rolling back application images, retain the new request table so retry history survives. Dropping it removes API run lookup and deduplication history, even though destination records and source snapshots remain. Do not replay old batches after removing that history.
