# Integration patterns

Source: https://ceyo.ai/docs/signal/integration-pattern-guides

### 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
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/by-external-id/customer-acme-eu' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

**Use exact lookups**

External-ID lookups are case-sensitive. URL-encode the identifier in `/projects/by-external-id/{external_id}` or the corresponding nested location path.

**Use IDs in resource paths**

Project and location path parameters accept either a Signal UUID or the configured external ID. A location identifier is resolved only inside its parent project.

**Reconcile before writing**

Look up the resource, create it after a `404`, and patch only fields that changed when it already exists.

> **Changing an external ID**
>
> Project and location updates can replace `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
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
      }
    ]
  }'
```

**Native support**

Bulk project creation, bulk location creation, embed session creation, and embed session refresh require an `Idempotency-Key` header.

**Retry with the same key**

Repeating the same request returns the original operation or response. Reusing its key with a different request returns `409 idempotency_conflict`.

**Client-managed create**

Single project and location create endpoints do not provide native idempotency keys. Look up a stable external ID before creating; if concurrent creation returns `409`, look up that external ID again.

> **Do not rotate keys during an uncertain retry**
>
> A new key represents a new intended native-idempotent operation. Keep the original key when a timeout or connection failure leaves the first request's result unknown.

### 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`.

```javascript
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;
}
```

> **Use a deterministic sort**
>
> Project and location lists use the resource ID as an ascending final tie-breaker. Keep the same filters, sort, and direction throughout one scan. A page beyond the result set returns an empty array.

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.

```javascript
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);
}
```

**Continue polling**

`pending` and `running` are non-terminal operation statuses.

**Stop polling**

`completed`, `completed_with_errors`, and `failed` are terminal statuses.

**Handle partial success**

One failed bulk item does not roll back successful items. Store each returned resource ID and retry failed records deliberately.

### Handle deletion and lifecycle changes

Project and location deletion is asynchronous. A successful request returns `202 Accepted` with no response body.

```curl
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 body
```

**Project deletion**

The project enters a deleting state and disappears from ordinary public project reads while its locations and scoped resources are purged.

**Location deletion**

The location is archived and its visibility scope is made inactive before its location-scoped resources are purged.

**Repeated requests**

Repeating a delete while the same deletion is active is accepted. After the resource has been purged, its path returns `404`.

> **Track deletion in your system**
>
> The delete response does not include an operation ID or completion status. Record that deletion was requested. Do not treat a project `404` as proof that purging has finished, and retry a conflicting recreation later.

> **Lifecycle fields are read-only**
>
> Project and location updates reject `status`. Use the supported delete endpoint for removal and treat returned lifecycle status as server-managed state.
