Skip to main content
Use the batch ingest to sync contacts from a CRM, a spreadsheet export, n8n or your own backend into a Salesflow pipeline, many at a time:
  • Batch ingest clients into a pipelinePOST /api/v2/public/salesflow/ingest/{input_node_uuid}/batch
  • List pipelinesGET /api/v2/public/salesflow/pipelines
  • List API input nodes of a pipelineGET /api/v2/public/salesflow/pipelines/{pipeline_id}/input-nodes
  • List stages in a pipelineGET /api/v2/public/salesflow/pipelines/{pipeline_id}/stages
For one contact at a time — a website form, a single webhook — use Ingest a client into a pipeline instead. Both endpoints are addressed to the same thing: an API input node.
The input node owns the destination. You create it once in the Salesflow editor (open the pipeline → input nodes → type API). It decides which stage new contacts land on, how they are assigned to your team, and the default lead value. The batch endpoint adds what a bulk sync needs on top: matching, a duplicate policy, custom fields, tags, a dry run and all-or-nothing mode.

Concepts

From API key to ingest in three calls

1

Find the pipeline

2

Find the node — and copy its ingest path

Only API-type nodes are listed — the ones the ingest endpoints can be addressed to. If the list is empty, create one in the Salesflow editor.
3

(Optional) pick another stage

List stages in a pipeline gives you every stage_id. Send one as stage_id in the batch to land contacts somewhere other than the node’s target stage.

A batch, end to end

Rows succeed or fail individually. A 200 means the request was valid and processed — read summary for the totals and results[] for what happened to each row, in request order. external_id is echoed back untouched, so correlate on it rather than on array position. pipeline.status is added (new card), reactivated (the contact had been removed from the board and is back), already_active (already on the board — the card stays where it is, no second card) or not_attempted (the row failed, or this was a dry run).

How contacts are matched

Each row is matched against your existing contacts by, in order:
  1. client_id — exact. Send it once you know it; it survives a changed phone or email.
  2. Phone number — country-code tolerant (see below).
  3. Email — case-insensitive.
A row that matches nothing is created. A row whose phone matches one contact and whose email belongs to another is refused with conflict — writing it would attach data to the wrong person.

Phone numbers

Phones are normalised to country code + national digits and stored that way. Send them in whichever shape you have:
  • +E.164 (+525512345678) is authoritative — nothing else is consulted.
  • National digits + a country code (5512345678 with area_code: "52", or defaults.area_code: "52" for the whole batch) — the recommended shape for CSV-style sources.
  • Bare international digits (525512345678) are accepted only when they form a valid number somewhere. A 9-digit local number without a country code is never guessed as some other country’s number — it fails with invalid_format instead.
Matching tolerates the shapes already in your account: a stored +52 55 1234 5678 is found whether you send E.164, national digits with a country code, or the full digits. One rule to know: phone uniqueness is on the national digits, so the same digits under two different country codes cannot coexist — such a row is reported as conflict with conflicting_client_id pointing at the owner.

Duplicate policy

on_duplicate decides what a match does:
This default is the opposite of the single-row endpoint’s. The batch defaults to on_duplicate: "update"; Ingest a client into a pipeline defaults to update: false (place, but do not touch). Set the policy explicitly if you move an integration from one to the other.

Merge rules

An update is a merge, never a replace:
  • Only non-empty row fields are written. An absent, null or blank field leaves the stored value alone — you cannot clear a field through this endpoint.
  • phone_number and area_code never change. The phone is the identity; change it in the dashboard.
  • timezone is written only when the row supplies it. A zone inferred from the phone or taken from defaults.timezone applies to new contacts only.
  • Custom fields (data) merge key by key — JSON Merge Patch style. A key in the row sets it; a key set to null removes it; keys you do not mention are kept. defaults.data is applied first, then the row’s own keys win. Keys are strings of at most 100 characters; the merged object must stay under 16 KB.
  • Tags are appended. Each tag is matched by exact name and created if missing (color and description apply to new tags only). Tags someone added by hand are never removed.

Preview with dry_run

Send "dry_run": true to get the same response — every row’s status, matches and errors — without writing anything. Matched rows additionally carry existing_client, a snapshot of the stored contact before any merge, so you can show a “current → new” preview. pipeline.status is not_attempted for every row.

All or nothing with all_or_none

By default the good rows land even when others fail. With "all_or_none": true, every row is still evaluated (so you see every problem at once) and then, if at least one failed, the whole batch is rolled back:
details.summary and details.results have exactly the shape of a 200 body, so one parser serves both. The statuses describe what would have happened; ids the rollback discarded (client_id of a created row, client_state_id of an added card) are omitted. Fix the failing rows and resend the whole batch.

What happens on placement

For every row placed as added or reactivated:
  • The stage’s on-arrival automations fire — sequences, messages, whatever the stage does when a contact arrives — unless you send "trigger_automations": false. That switch is for silent back-fills; a live sync should leave it on so imported leads are worked like any other.
  • The input node’s assignment rules run (strategy and pool), exactly as for a lead that arrived any other way.
  • The node’s default_lead_value is written to the client’s value if the client has none yet.
  • The card’s entry_source is api and its entry log points at the node.
Nothing above happens for already_active rows: the contact was already on the board and their card, stage and progress are left alone. Re-ingesting a contact who is mid-pipeline is therefore safe — it updates their record (under update) and does not move them.

Limits and good practice

  • 500 rows per request. Split larger sets into consecutive batches (422 above the cap).
  • Synchronous. A 500-row batch normally completes in a few seconds. Do not run batches in parallel against the same node.
  • Retries are safe — and you do not need an idempotency key. Re-running the same rows with on_duplicate: "update" converges: rows that were created come back updated / already_active, nothing is duplicated. If a request timed out and you do not know whether it landed, send it again.
  • Set defaults.area_code whenever your source holds national numbers. Without a country code a local number is rejected rather than guessed.
  • Be explicit about on_duplicate. update for a sync; skip for a one-off import that must not overwrite hand-edited data; fail when a duplicate means your source has a problem.
  • Prefer client_id once you have it. Store the client_id from each result next to your own record; later rows can then update that person even if their phone or email changed on your side.

Wiring it into n8n

1

HTTP Request node

Method POST, URL https://api.dialtu.com + the ingest_path from List API input nodes of a pipeline.
2

Authentication

Generic Credential TypeHeader Auth, name Authorization, value Bearer pk_xxxxxxxx.
3

Body

Body Content Type JSON. Build clients from your earlier nodes (an Aggregate or Code node turns items into one array; stay under 500 per run and use Split In Batches above that). Put your own record id in external_id.
4

After the call

Loop over results[]: store client_id on your side for created / updated rows; route failed rows by errors[0].code. A 400 INGEST_ROLLED_BACK (only with all_or_none) means nothing landed — fix and resend. See Errors.

Not through this endpoint

Moving a contact who is already on the board is not what an ingest does — use Move a client between stages. Creating API input nodes, and clearing a stored field to empty, are done in the dashboard.