- Batch ingest clients into a pipeline —
POST /api/v2/public/salesflow/ingest/{input_node_uuid}/batch - List pipelines —
GET /api/v2/public/salesflow/pipelines - List API input nodes of a pipeline —
GET /api/v2/public/salesflow/pipelines/{pipeline_id}/input-nodes - List stages in a pipeline —
GET /api/v2/public/salesflow/pipelines/{pipeline_id}/stages
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
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
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:client_id— exact. Send it once you know it; it survives a changed phone or email.- Phone number — country-code tolerant (see below).
- Email — case-insensitive.
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 (
5512345678witharea_code: "52", ordefaults.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 withinvalid_formatinstead.
+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:
Merge rules
An update is a merge, never a replace:- Only non-empty row fields are written. An absent,
nullor blank field leaves the stored value alone — you cannot clear a field through this endpoint. phone_numberandarea_codenever change. The phone is the identity; change it in the dashboard.timezoneis written only when the row supplies it. A zone inferred from the phone or taken fromdefaults.timezoneapplies 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 tonullremoves it; keys you do not mention are kept.defaults.datais 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 (
coloranddescriptionapply 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 asadded 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_valueis written to the client’s value if the client has none yet. - The card’s
entry_sourceisapiand its entry log points at the node.
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 (
422above 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 werecreatedcome backupdated/already_active, nothing is duplicated. If a request timed out and you do not know whether it landed, send it again. - Set
defaults.area_codewhenever your source holds national numbers. Without a country code a local number is rejected rather than guessed. - Be explicit about
on_duplicate.updatefor a sync;skipfor a one-off import that must not overwrite hand-edited data;failwhen a duplicate means your source has a problem. - Prefer
client_idonce you have it. Store theclient_idfrom 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 Type → Header 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.