Skip to main content
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, calculations, approval workflow, and source-row history as imports on the /data page. The API endpoint determines duplicate handling: /source-imports inserts new IDs only, and /source-imports/amendments updates existing IDs only. File-import defaults do not override these rules. This guide covers the source adapter REST API, not the MCP server or direct activity creation.

Before you start

Your installation must include the current JSON source import API release and its database migrations. Check your installation’s /api/v1/openapi.json for /source-adapters, /source-imports, and /source-imports/amendments, including the outcomes and row results response fields. Earlier API releases may expose only part of this contract. Publishing this guide does not upgrade an installation. See 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:
For Azalt Cloud, use https://app.azalt.co. 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.
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. Before submitting either new rows or amendments, configure Row ID fields or ensure the adapter script provides stable row IDs. Declaring invoice_id and line_id in the input contract does not select them as the row ID fields. See row ID configuration below. apiReady: true confirms an active adapter with an enabled contract; it does not guarantee valid row IDs or destination mappings for your data.

Accepted data types

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

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:
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:
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.

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.
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:
The rows response contains rows and total. Each row includes sheetName, rowIndex, rawRow, status, warnings, error, externalRowIds, and results. Pagination defaults to 100 rows and is limited to 200. Increase offset by the number of rows received until you have read total rows. The optional status=failed filter selects stored row statuses only; total then counts only matching rows. Preview conflicts can still have status: "ready". Fetch rows without a status filter and inspect results for phase: "planned" with action: "failed", as well as row errors. Check outcomes.planned.failed on the run response too. readiness checks missing form/site/year deployments. It does not validate business-ID matches or guarantee successful execution. Amendment drafts are excluded from that deployment check because amendments cannot create missing targets. A preview’s execution.attempted: false and execution.blocked: false do not mean its rows are valid.

4. Execute the saved preview

After reviewing the mapped results and readiness, execute the same run:
For insertions, 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. This confirmation permits creating missing form deployments; it cannot make an unmatched amendment succeed. Only after reviewing and approving those missing form deployments, repeat the execute request with this body:
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, outcomes, and individual row results, warnings, and errors. One source row can produce multiple drafts. A row marked failed can still contain successful drafts, so inspect each result before deciding what needs correction. 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.

Amend previously imported rows

Owners select Row ID fields from a searchable column list in Customization → Source adapters. The list comes from the API contract, adapter column settings, or Load columns from a sample file (CSV/Excel, all sheets; only headers are saved). Unknown column names cannot be added or saved. Select stable fields that uniquely identify a row in your source. For example, invoice lines can use invoice_id and line_id together. Adapter configuration (inputFormat): Save these settings on the adapter. This block is not a request body for /source-imports or /source-imports/amendments.
The sourceColumns list above represents headers read from a sample file; it is not needed when the fields are already declared in the API contract or adapter column settings. Replace these example field names with the exact headers from your own source. Include the selected ID columns in the JSON API input contract. IDs preserve leading zeros; send them as strings. match: "all" requires all fields. match: "first" checks fields in the configured order, skipping missing values, null, and empty strings. Whitespace-only strings and values other than strings or safe integers are invalid, rather than skipped. Contract validation happens first: missing required columns or invalid field types reject the request with HTTP 400. After that, missing or invalid business IDs are reported as row errors during preview mapping. These settings belong to the adapter’s inputFormat. GET /source-adapters/{id} exposes the configured rowIdentity and file-import onExistingRow default. The API endpoint determines the write operation; adapter defaults cannot override it. All paths use /api/v1. Neither endpoint accepts onExistingRow in the request body. Sending it returns HTTP 400 with guidance to the amendment endpoint. Both endpoints require configured ID fields or adapter-provided stable IDs. IDs and destination mapping come from the source adapter; inspect row errors as well as planned outcomes. File imports retain an explicit When an existing row is encountered option: insert_only rejects existing IDs, update_only rejects unmatched IDs, update updates matches and inserts new IDs, and skip keeps matches while inserting new IDs. append retains legacy behavior (new activities, or replacement form-value totals). These file-import choices do not change the API endpoint’s behavior. Matching is scoped to the adapter and destination: form, element, site, year and period. A correction changes data within that destination. Changing an ID or routing a row to another destination represents a new row, which update-only rejects. Activity updates keep the internal activity and submission IDs, recalculate values, and follow approval rules. Form-value updates replace the matching row’s contribution and recalculate the full total, retaining other rows. Send each business ID only once per destination in an amendment batch. Repeated matching IDs can produce one update and skipped drafts; do not rely on which occurrence wins across sheets. Ambiguous duplicates already stored are rejected. GET /source-imports/{id}/rows returns externalRowIds for inspecting business IDs. Existing activity rows can be matched from their saved source cells; older form-value imports use their saved draft history. Corrections require that this source information remains available. Send corrections with a new Idempotency-Key. That header identifies a batch request; row IDs identify business records across batches. ID settings and default behavior are included in adapterVersion. Saved previews retain their ID settings and behavior even if the adapter is subsequently edited.

Insert new rows only

POST /source-imports never updates an existing source row’s contribution. A matching ID fails with errorCode: "ROW_ALREADY_EXISTS"; its message explains that corrections belong at POST /api/v1/source-imports/amendments. The row results also expose the business ID, source row ID and destination. If an ID repeats within the same destination in one insert batch, all its occurrences fail with DUPLICATE_ROW_ID. Send each new row once. If that ID already exists, ROW_ALREADY_EXISTS takes precedence. A repeated batch request using the same Idempotency-Key and body is a retry and returns the saved run; it does not count as a new insertion attempt. For form values, inserting a new source contribution can recalculate an existing aggregate record. Previously imported contributions are retained and their values are not replaced. If the destination has a value but this adapter has no source contribution history for it, UNTRACKED_FORM_VALUE prevents an accidental overwrite. Reconcile the destination’s source history before adding contributions. Preview conflicts appear in outcomes.planned.failed and each row’s results. A row may still have status: "ready" because mapping succeeded. During execution, conflicting drafts become failed and valid drafts can still insert. HTTP 200 does not mean every row succeeded: inspect outcomes.actual.failed and GET /source-imports/{id}/rows. Matches are checked again during execution under the adapter lock, including when another batch inserted the ID after preview.

Migrating an existing API client

Remove onExistingRow from JSON requests. Send new rows to /source-imports and complete corrected rows to /source-imports/amendments. Split mixed create/update batches by operation. Keep both the endpoint and body unchanged for retries; using the same key for an insertion and an amendment returns 409. Previously saved previews retain their original behavior. Inspect them before execution, or create a new preview through the intended endpoint. Requests saved under the previous contract may no longer replay with the edited body; use their saved run IDs to inspect their status rather than resubmitting successful data.

Update existing rows only

Send the complete corrected source rows, retaining their configured business IDs and destination fields, to POST /source-imports/amendments. The body uses the same source-row contract as an insertion, and only allows allowMissingFormSiteCreation: false. The server enforces update-only even when the adapter default allows insertion.
Use a fresh Idempotency-Key for each correction; reuse the same key and body for retries. The endpoint creates an amendment run and defaults to preview. After review, execute the returned run with POST /source-imports/{id}/execute, or send mode: "execute" initially. The original destination row must already have been imported; an earlier preview alone creates nothing to amend. An unmatched ID or destination fails that mapped row. It never creates a new activity, form-value contribution or destination record. Other valid rows in the batch can succeed, so inspect results even on HTTP 200. Missing targets are checked again during execution. Saved previews retain the update-only rule even if the adapter default changes. This is a POST because it creates a batch-processing run with history and per-row results. It does not accept a partial JSON Patch document. A PATCH operation on an identified record would be a different contract and would need the atomic application required by the HTTP PATCH specification.

Distinguish inserts from updates

Submission, execution and GET /source-imports/{id} responses include: GET /source-imports/{id}/rows includes a results array for each source row. Each result contains phase (planned or actual), action, externalRowId, draftId, sourceImportRowId, destination fields (kind, formId, targetKey, siteId, year, periodUnit), activityId, formElementSubmissionId, errorCode and any error message. errorCode is null when no specific error code is available; always inspect message. Planned actions use the same action names, such as updated, but are estimates. A planned insertion into a new destination record has no created record ID yet. A new form-value contribution to an existing aggregate can already include that aggregate’s formElementSubmissionId. Estimates are read-only and can change when destination data changes; execution rechecks the matches. Outcome counts are per mapped draft. One source row can produce several drafts, including multiple drafts for the same destination. These counts do not count distinct source rows or destination records. For form-value imports, inserted/updated refers to the row’s contribution; several contributions can share the same aggregate record. run.activityWriteCount and run.formValueWriteCount count destination writes. counts.imported still combines successful inserts and updates, and counts.reduced describes contributions combined into another row’s total. Source rows with no mapped drafts remain visible through the original row counts, status and errors. Do not use replayed to identify updated rows: it identifies batch-request retries. Execution records the action and destination IDs in the same transaction as the write. These results remain available even if the destination changes later. File-import previews and history show the same expected and recorded outcomes.

Changing the row ID rule after importing data

Saving new ID fields, changing their order, or switching between all and first does not rewrite existing rows or historical import IDs. Permanent database IDs remain unchanged. There is no automatic migration or check of existing rows for compatibility when the new rule is saved.
  • Activities: On a later update/skip submission, Azalt derives matching IDs from existing activities’ saved source cells using the new rule. A unique match can be updated while retaining its database ID. If the saved source lacks the required fields, that activity cannot match. The amendment endpoint rejects that unmatched row. Insert-only API submissions and file imports that allow insertion can treat it as new and create a duplicate. Multiple existing matches are rejected.
  • Form values: Older imports can be matched using their saved source drafts. Once a target has a stored amendment snapshot, changing its ID rule blocks further amendments. Use the file-import workflow to import all contributing rows for that form-value target as a complete replacement with the append option before using the new rule for updates. The JSON API does not support this replacement operation and rejects onExistingRow in request bodies. This replacement behavior applies to form values; append creates new activities.
  • API versions and previews: Fetch the current adapterVersion after changing the rule. New submissions with a stale version return 409. Existing saved previews continue to use their original rule.

Retries and adapter changes

  • Idempotency-Key is mandatory for POST /source-imports and POST /source-imports/amendments: 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. General POST rejects matching business IDs; amendments update them. Retry an unchanged logical batch with its original key instead of creating a new batch.
  • 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.

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. String length is measured in UTF-16 code units; characters such as emoji can count as two. Split larger datasets into separate batches with distinct keys. Your ingress or hosting provider may enforce a lower body size or timeout limit. 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 all pending migrations bundled with the release using the normal Helm migration job/process. These include 20260908000000_add_source_import_api_requests.ts, which 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.