Integration patterns
Keep partner data synchronized while handling retries, pagination, background work, and resource deletion predictably.
Synchronize resources with external IDs
Assign each project and location the stable identifier from your own system. Project external IDs are unique within the workspace. Location external IDs are unique within their parent project.
curl --request GET \
--url 'https://api.signal.ceyo.ai/v1/projects/by-external-id/customer-acme-eu' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}"/projects/by-external-id/{external_id} or the corresponding nested location path.404, and patch only fields that changed when it already exists.external_id. Save the returned value before the next synchronization because the previous external-ID lookup will no longer match.Build idempotent provisioning flows
Choose one idempotency key for one intended operation and persist it with the job that sends the request.
curl --request POST \
--url 'https://api.signal.ceyo.ai/v1/projects/bulk' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: sync-run-2026-08-04-001' \
--data '{
"projects": [
{
"name": "Acme Europe",
"external_id": "customer-acme-eu",
"package_id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
"website": "https://acme.example",
"start": false
}
]
}'Idempotency-Key header.409 idempotency_conflict.409, look up that external ID again.Paginate and poll efficiently
List endpoints use 1-based offset pages. The default page size is 25, the maximum is 100, and every response includes page, per_page, total, and total_pages.
let page = 1;
while (true) {
const response = await fetch(
`https://api.signal.ceyo.ai/v1/projects?page=${page}&per_page=100&sort=created_at&direction=asc`,
{ headers: { Authorization: `Bearer ${process.env.SIGNAL_API_KEY}` } }
);
if (!response.ok) throw new Error(`List failed: ${response.status}`);
const { projects, pagination } = await response.json();
await synchronizePage(projects);
if (page >= pagination.total_pages) break;
page += 1;
}Bulk creates return 202 Accepted with a status_url. Poll that URL until the operation reaches a terminal status, then inspect every item.
Project and location creates with start: true return an onboarding_operation. Poll its status_url until it succeeds, completes partially, fails, or is cancelled.
const terminal = new Set([
"completed",
"completed_with_errors",
"failed",
]);
let operation;
do {
const response = await fetch(
`https://api.signal.ceyo.ai${statusUrl}`,
{ headers: { Authorization: `Bearer ${process.env.SIGNAL_API_KEY}` } }
);
if (!response.ok) throw new Error(`Poll failed: ${response.status}`);
({ bulk_operation: operation } = await response.json());
if (!terminal.has(operation.status)) await new Promise((r) => setTimeout(r, 2000));
} while (!terminal.has(operation.status));
for (const item of operation.items) {
if (item.status === "failed") recordFailure(item.index, item.error);
}pending and running are non-terminal operation statuses.completed, completed_with_errors, and failed are terminal statuses.Handle deletion and lifecycle changes
Project and location deletion is asynchronous. A successful request returns 202 Accepted with no response body.
curl --request DELETE \
--url 'https://api.signal.ceyo.ai/v1/projects/customer-acme-eu' \
--header "Authorization: Bearer ${SIGNAL_API_KEY}"
# HTTP/1.1 202 Accepted
# No response body404.404 as proof that purging has finished, and retry a conflicting recreation later.status. Use the supported delete endpoint for removal and treat returned lifecycle status as server-managed state.