> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ringee.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Logs and errors

> Debug inbound events and outbound deliveries

Accepted inbound events and queued outbound deliveries are inspectable from the dashboard or the management API. A duplicate inbound `eventId` is returned as `skipped` without creating another row, and the immediate `test.ping` action is not stored as a delivery.

## Where to look

| Symptom | Look at |
| - | - |
| Your event never appeared in Ringee | **Inbound logs** |
| Ringee never called your endpoint | **Outbound logs** |
| Ringee called you but nothing was created | Your own idempotency table — the event was probably deduplicated |
| Everything stopped at once | Integration status, and whether the API key was rotated |

In the dashboard: **Integrations → Custom Integrations → Configure**, then the inbound and outbound log tabs.

## Log endpoints

Both require an admin dashboard session and return newest-first arrays.

```http theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
GET /api/integrations/custom/:id/inbound-logs?limit=50&cursor=…
GET /api/integrations/custom/:id/outbound-logs?limit=50&cursor=…
```

`limit` defaults to 50 and is capped at 200. To load the next page, pass the last returned row's `id` as `cursor`.

**Inbound rows** contain `id`, `integrationId`, `eventType`, `externalEventId`, `status`, `receivedAt`, `processedAt`, `errorMessage` and `rawPayload`. Their normal lifecycle is `received` → `processing` → `processed` or `failed`.

**Outbound rows** contain `id`, `integrationId`, `eventType`, `subjectId`, `destinationUrl`, `payload`, `signature`, `status`, `attemptCount`, `lastError`, `nextAttemptAt`, `sentAt`, `dedupeKey`, `createdAt` and `updatedAt`. Status is `pending`, `sending`, `sent` or `failed`.

<Note>
  Outbound logging is one aggregate row per delivery, not one row per attempt.
  `attemptCount` counts failed attempts and `lastError` stores only the most
  recent failure. HTTP status and latency are returned by **Test webhook**, but
  are not persisted for each queued attempt.
</Note>

## Inbound errors

### Rejected before Ringee records anything

A bad key or an incomplete envelope is answered synchronously, and nothing reaches the inbound log. Every envelope problem is reported in one message, joined by `; `:

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "statusCode": 400,
  "message": "eventId is required; occurredAt is required; data must be an object",
  "error": "Bad Request"
}
```

| Status | Message | Fix |
| - | - | - |
| `401` | `Invalid API key format` | The header is missing or the value is not a well-formed `cik_live_` key. |
| `401` | `Invalid API key` | The key does not match any integration — it was likely rotated. |
| `401` | `Integration is disabled` | Re-enable the integration in the dashboard. |
| `400` | `body must be a JSON object` | Send `Content-Type: application/json` and a JSON object. |
| `400` | `event is required` | Add `event` to the envelope. |
| `400` | `eventId is required` | Add a unique `eventId`. |
| `400` | `occurredAt is required` | Add an ISO-8601 `occurredAt`. |
| `400` | `data must be an object` | `data` must be a JSON object, even when nearly empty. |
| `400` | `Unsupported event: …` | The message lists the four event names Ringee accepts inbound. |

### Recorded, then rejected

A `202` with `"status": "failed"` means the event was authenticated and stored, then rejected while being applied. It is in the inbound log with the reason in `errorMessage`, and **nothing was written**: validation runs before the first record is touched.

| Message | Cause |
| - | - |
| `data.<field> is required` | A required field is missing or blank. All of them are listed at once. |
| `data.<field> must be a string \| an object` | A required field has the wrong JSON type. |
| `data.campaignId must be a UUID` | The id is not a UUID. Ids are never ignored silently. |
| `data.phoneNumber is not a dialable number` | Nothing in the value could be read as a phone number. |
| `Campaign not found` / `Access denied` | `data.campaignId` matches no campaign in this workspace. |
| `Campaigns require an organization` | `data.campaignId` was sent from a personal workspace, which has no campaigns. |

<Warning>
  Resending a failed event with the **same** `eventId` returns `skipped`, not a
  retry. Fix the payload and send it with a **new** `eventId`.
</Warning>

### Applied, with warnings

A `processed` response can carry a `warnings` array — an unknown field name, a phone number read with an assumed country, an `ownerEmail` matching nobody. The event applied; something in it did not. See [Inbound events](/api/inbound-events#warnings).

<Note>
  Warnings are returned in the response only. They are not stored on the inbound
  log row, so capture them when you send.
</Note>

## Outbound delivery failures

A delivery is retried when your endpoint returns a non-2xx status, times out after 15 seconds, or is unreachable.

| Recorded error | Meaning |
| - | - |
| `HTTP 4xx` / `HTTP 5xx` | Your endpoint responded with a non-2xx status. |
| Timeout or network error | No response within 15 seconds, or the host could not be reached. |
| `integration not found` | The integration was deleted while the delivery was queued. |
| `integration disabled` | The integration's status is no longer `active`. |
| `no outbound URL configured` | Returned by **Test webhook** when no URL is set. |

After 10 failed attempts the delivery is marked `failed`. Ringee sends email and device push notifications to the integration owner or organization admins, throttled to at most one alert per integration every six hours.

## Common causes

<AccordionGroup>
  <Accordion title="Signature verification always fails">
    You parsed the body before verifying. Read the **raw text** and HMAC exactly those bytes — a `JSON.parse` followed by `JSON.stringify` produces different bytes and a different digest.

    Also confirm you are hashing `` `${timestamp}.${rawBody}` ``, not the body alone, and that you are using the `whsec_` signing secret rather than the `cik_live_` API key.
  </Accordion>

  <Accordion title="Everything returns 401 after a rotation">
    Rotating the API key invalidates the old one immediately — and also revokes every Dialer SDK publishable key on that integration. Update both.
  </Accordion>

  <Accordion title="Contacts are duplicated in Ringee">
    You are sending a different `externalId` for the same record, or omitting it. Dedup runs on `externalId` first, then `phoneNumber`. Keep `externalId` stable for the lifetime of the record.
  </Accordion>

  <Accordion title="Fields are not being cleared">
    That is intentional. `null`, `undefined` and empty strings never overwrite existing values on upsert — this protects Ringee data from partial CRM payloads.
  </Accordion>

  <Accordion title="Your endpoint receives the same event twice">
    Expected. Transport retries reuse the `eventId`. Deduplicate on it with a
    `UNIQUE` constraint — see [Outbound
    webhooks](/api/outbound-webhooks#idempotency).
  </Accordion>

  <Accordion title="An AI Voice Agent event is not linked to your record">
    Send your record id as `metadata.external_id` or `metadata.externalId` when you
    start the call. Ringee exposes it as `data.externalId` on every call-linked
    outbound event. `data.agent.id` identifies the Ringee AI Voice Agent, not your
    CRM record.
  </Accordion>

  <Accordion title="Recordings never arrive">
    `recording.ready` is a separate opt-in event that can arrive minutes after `call.completed`. Confirm it is selected in the outbound event list, and that recording is enabled for the workspace.
  </Accordion>
</AccordionGroup>

## Self-hosted debugging

When running Ringee yourself, deliveries are drained by the Temporal orchestrator (`apps/orchestrator`). If outbound events queue up but never send, check that the orchestrator is running and connected to Temporal — see [How it works](/howitworks).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.