curl --request POST \
--url https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"apply": true,
"conversion_id_column": "<string>",
"default_calling_code": "<string>",
"email_column": "<string>",
"operation_key": "<string>",
"phone_column": "<string>"
}
'import requests
url = "https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain"
payload = {
"apply": True,
"conversion_id_column": "<string>",
"default_calling_code": "<string>",
"email_column": "<string>",
"operation_key": "<string>",
"phone_column": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
apply: true,
conversion_id_column: '<string>',
default_calling_code: '<string>',
email_column: '<string>',
operation_key: '<string>',
phone_column: '<string>'
})
};
fetch('https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'apply' => true,
'conversion_id_column' => '<string>',
'default_calling_code' => '<string>',
'email_column' => '<string>',
'operation_key' => '<string>',
'phone_column' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain"
payload := strings.NewReader("{\n \"apply\": true,\n \"conversion_id_column\": \"<string>\",\n \"default_calling_code\": \"<string>\",\n \"email_column\": \"<string>\",\n \"operation_key\": \"<string>\",\n \"phone_column\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"apply\": true,\n \"conversion_id_column\": \"<string>\",\n \"default_calling_code\": \"<string>\",\n \"email_column\": \"<string>\",\n \"operation_key\": \"<string>\",\n \"phone_column\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"apply\": true,\n \"conversion_id_column\": \"<string>\",\n \"default_calling_code\": \"<string>\",\n \"email_column\": \"<string>\",\n \"operation_key\": \"<string>\",\n \"phone_column\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"automation_inventory": {
"compatible": 123,
"future_consumers": 123,
"future_reasons": [
{
"code": "<string>",
"count": 123
}
],
"incompatible": 123,
"queued": 123,
"reasons": [
{
"code": "<string>",
"count": 123
}
],
"required_changes": [
{
"job_id": "<string>",
"reason_code": "<string>",
"status": "<string>"
}
],
"running": 123,
"unverifiable": 123
},
"blockers": [
"<string>"
],
"maintenance": {
"assigned_count": 123,
"published_storage_key": "<string>",
"result_digest": "<string>",
"row_count": 123,
"source_digest": "<string>",
"source_storage_item_id": "<string>",
"source_storage_key": "<string>",
"state": "<string>"
},
"pipeline_inventory": {
"adoptable": [
"<string>"
],
"compatible": 123,
"in_flight": 123,
"incompatible": 123,
"reasons": [
{
"code": "<string>",
"count": 123
}
],
"unverifiable": 123
},
"pipelines_adopted": [
"<string>"
],
"ready": true
}{
"code": "not_found",
"details": {},
"message": "<string>"
}MaintainLeadIdentityAPI is the REST surface for previewing and performing a
dataset’s permanent-lead-identity migration.
It mirrors eventpipeline.MaintainCanonicalLeadAdoption, which is an `auth` endpoint outside /v1 and therefore reachable only from a signed-in browser session: API keys and scoped tokens are confined to /v1, /mcp and the documented SDK routes (backend/middleware/external_surface.go). The migration shipped with no operator surface at all — no UI calls it, the CLI speaks /v1, and an API key is refused — so the only way to run it was a fetch() pasted into DevTools. That is not a supported way to migrate a customer’s leads.
The handler stays thin on purpose: the ordering that keeps adoption, backfill and the identity config consistent lives in the eventpipeline service, and this endpoint brings only its own RBAC (dataset-level edit permission check) and a slug-addressed path. The dataset is resolved via GetDatasetByIdentifier to obtain the dataset ID for the RBAC check, which accepts a slug or a UUID, so callers can use whichever they hold.
A scoped API key is refused, for the reason the lead merge refuses one and then some. erdo_token_* credentials are per-end-user, and /v1/… admits them wholesale, while the only org check downstream is CanAccessOrgResource — plain equality, which an external user with OrganizationID set satisfies. Without this guard a single lead’s own credential could migrate the dataset holding every other lead: rewrite the file, rewrite every pipeline that writes to it, and publish an identity config that cannot be withdrawn.
curl --request POST \
--url https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"apply": true,
"conversion_id_column": "<string>",
"default_calling_code": "<string>",
"email_column": "<string>",
"operation_key": "<string>",
"phone_column": "<string>"
}
'import requests
url = "https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain"
payload = {
"apply": True,
"conversion_id_column": "<string>",
"default_calling_code": "<string>",
"email_column": "<string>",
"operation_key": "<string>",
"phone_column": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
apply: true,
conversion_id_column: '<string>',
default_calling_code: '<string>',
email_column: '<string>',
operation_key: '<string>',
phone_column: '<string>'
})
};
fetch('https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'apply' => true,
'conversion_id_column' => '<string>',
'default_calling_code' => '<string>',
'email_column' => '<string>',
'operation_key' => '<string>',
'phone_column' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain"
payload := strings.NewReader("{\n \"apply\": true,\n \"conversion_id_column\": \"<string>\",\n \"default_calling_code\": \"<string>\",\n \"email_column\": \"<string>\",\n \"operation_key\": \"<string>\",\n \"phone_column\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"apply\": true,\n \"conversion_id_column\": \"<string>\",\n \"default_calling_code\": \"<string>\",\n \"email_column\": \"<string>\",\n \"operation_key\": \"<string>\",\n \"phone_column\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/datasets/{datasetSlug}/lead-identity/maintain")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"apply\": true,\n \"conversion_id_column\": \"<string>\",\n \"default_calling_code\": \"<string>\",\n \"email_column\": \"<string>\",\n \"operation_key\": \"<string>\",\n \"phone_column\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"automation_inventory": {
"compatible": 123,
"future_consumers": 123,
"future_reasons": [
{
"code": "<string>",
"count": 123
}
],
"incompatible": 123,
"queued": 123,
"reasons": [
{
"code": "<string>",
"count": 123
}
],
"required_changes": [
{
"job_id": "<string>",
"reason_code": "<string>",
"status": "<string>"
}
],
"running": 123,
"unverifiable": 123
},
"blockers": [
"<string>"
],
"maintenance": {
"assigned_count": 123,
"published_storage_key": "<string>",
"result_digest": "<string>",
"row_count": 123,
"source_digest": "<string>",
"source_storage_item_id": "<string>",
"source_storage_key": "<string>",
"state": "<string>"
},
"pipeline_inventory": {
"adoptable": [
"<string>"
],
"compatible": 123,
"in_flight": 123,
"incompatible": 123,
"reasons": [
{
"code": "<string>",
"count": 123
}
],
"unverifiable": 123
},
"pipelines_adopted": [
"<string>"
],
"ready": true
}{
"code": "not_found",
"details": {},
"message": "<string>"
}Authorizations
An Erdo API key (erdo_api_...) or scoped token (erdo_token_...).
Path Parameters
Body
ready, what blocks it, and how many rows would be assigned an identity. Apply true performs the migration — adopt every pipeline writer, backfill the IDs and publish the identity config, in one transaction.
written two ways is one lead. Empty means "1".
from the dataset's own compatibility report when omitted.
same key settles an apply whose response was lost; a different key against a migrated dataset is refused rather than migrating a second time.
Response
Success response
identify the exact consumers the migration cannot make safe on its own; names, scripts, parameters and trigger payloads never cross the service boundary.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes

