Splashify RCS

Docs

Open panel
Panel

Event log

Every outbound webhook attempt is recorded, the URL, the attempt number, the status code your endpoint returned, how long it took, and the first bytes of the body. This is the audit trail behind the panel's Settings → Event log screen.

Use it to answer the two questions that come up whenever a webhook integration misbehaves: did you actually send it? and what did my server say back?

http
GET  /api/v1/rcs/webhooks/{webhook_id}/deliveries
GET  /api/v1/rcs/webhooks/dead-letter
POST /api/v1/rcs/webhooks/dead-letter/{delivery_id}/replay

There is no single /events endpoint

The panel's consolidated Event log is assembled client-side. It lists your endpoints, then reads each one's attempts. See Rebuilding the consolidated log for how to do the same thing, and what its limits are.

The Event log page with the tabs All (20), Succeeded (14) and Failed (6) over one table of attempts across two endpoints, each row with the event, the status (200, 401, 503 or Failed (no response)), the time taken, the attempt number, the endpoint and the time
The panel's Event log: attempts from every endpoint in one table, newest first.

Delivery attempts

GET

/api/v1/rcs/webhooks/{webhook_id}/deliveries

JWTany role

Returns the most recent attempts for one endpoint, newest first.

http
GET /api/v1/rcs/webhooks/48b21f9a-7e87-4057-bee7-662879612c21/deliveries?limit=100
Authorization: Bearer <jwt>
json
{
  "success": true,
  "deliveries": [
    {
      "attempt_id":       "9c2f1d80-8500-11ef-9c9a-4bc60d7a1e2f",
      "delivery_id":      "wd_5f0a3c22-8501-11ef-8f3a-4bc60d7a1e2f",
      "event_type":       "message.delivered",
      "url":              "https://api.example.com/webhooks/splashifypro",
      "attempt":          1,
      "status_code":      200,
      "took_ms":          143,
      "response_snippet": "{\"ok\":true}",
      "sent_at":          "2026-08-25T04:11:09Z"
    },
    {
      "attempt_id":       "7a10bb4e-84ff-11ef-9c9a-4bc60d7a1e2f",
      "delivery_id":      "wd_2b7c9910-84ff-11ef-8f3a-4bc60d7a1e2f",
      "event_type":       "broadcast.completed",
      "url":              "https://api.example.com/webhooks/splashifypro",
      "attempt":          3,
      "status_code":      503,
      "took_ms":          30012,
      "response_snippet": "upstream connect error",
      "sent_at":          "2026-08-25T03:52:41Z"
    }
  ]
}

Reading the outcome

There is no explicit outcome field, derive it from status_code, exactly as the panel does:

text
function outcomeOf(attempt) {
  // 2xx is success. Anything else failed, including 0: no HTTP answer at all.
  return attempt.status_code >= 200 && attempt.status_code < 300
    ? "succeeded"
    : "failed";
}

status_code 0 is a failure

An attempt is recorded once it has finished, so a zero never means "still going". It means your endpoint gave no HTTP answer: the request timed out, the connection was refused, or the domain did not resolve. response_snippet says which, and the attempt is retried like any other failure. The panel shows it as Failed (no response).

Access and scope

Any role can read deliveries. A team member scoped to specific bots can only read attempts for endpoints scoped to a bot they hold, requesting one outside their scope returns 403 bot not in scope, and an unknown endpoint returns 404.

This is a recent tail, not full history

The route caps at 500 rows and has no cursor. There is no way to page backwards through older attempts. For long-term retention, consume the attempts periodically and store them yourself, or pull the same information from your own server's access logs by correlating on delivery_id.

Dead-letter queue

GET

/api/v1/rcs/webhooks/dead-letter

JWToperator+

A delivery that fails its first attempt is retried three more times, at 1s, 5s and 30s. If all four fail, it is not retried again. It lands here with its full envelope preserved, waiting for you to replay it.

The Dead-letter queue page with three deliveries that failed every attempt, each with its event, the endpoint URL, the reason (http_503 or http_404), when it was first tried and given up on, and a Replay button
The same rows in the panel, at Settings, Webhooks, Dead-letter queue.
json
{
  "success": true,
  "items": [
    {
      "delivery_id":          "wd_2b7c9910-84ff-11ef-8f3a-4bc60d7a1e2f",
      "webhook_id":           "48b21f9a-7e87-4057-bee7-662879612c21",
      "event_type":           "broadcast.completed",
      "url":                  "https://api.example.com/webhooks/splashifypro",
      "reason":               "HTTP 503 after 4 attempts",
      "first_attempt_at":     "2026-08-25T03:52:11Z",
      "permanent_failure_at": "2026-08-25T03:52:47Z"
    }
  ]
}

Note the response key is items, not deliveries.

Scope filtering

For a caller scoped to all bots, every row is returned. For a scope-limited team member, rows are filtered server-side and a row is hidden when:

  • its endpoint has since been deleted, or
  • its endpoint is scoped to all bots (a scope-limited caller must not see account-wide traffic), or
  • its endpoint's bot is outside the caller's scope

This means two people can legitimately see different row counts on the same queue. That is the scope working, not a bug.

Replay a dead-letter delivery

POST

/api/v1/rcs/webhooks/dead-letter/{delivery_id}/replay

JWToperator+

Re-queues the archived envelope for delivery, with a fresh delivery_id and the attempt counter reset to 1, so it gets the full four-attempt retry schedule again.

http
POST /api/v1/rcs/webhooks/dead-letter/wd_2b7c9910-84ff-11ef-8f3a-4bc60d7a1e2f/replay
Authorization: Bearer <jwt>
json
{
  "success": true,
  "new_delivery_id": "wd_8e41c07a-8600-11ef-8f3a-4bc60d7a1e2f"
}

The original row is deleted from the queue as part of a successful replay, so the same delivery cannot be replayed twice by accident. Track new_delivery_id if you want to follow the replay through the attempts endpoint.

The payload is replayed as originally sent, same event, same data, same occurred_at semantics. It is not re-derived from current state, so replaying a broadcast.completed from last week delivers last week's counters.

Make your consumer idempotent before replaying

A replay is a genuinely new delivery of an event your server may already have partially processed before it failed. Deduplicate on delivery_id from the original attempt, or on the business fact in data, do not assume a replayed event is one your server has never seen.

Rebuilding the consolidated log

The panel's Event log shows attempts across every endpoint in one table. There is no server-side endpoint for that. It is assembled client-side, and you can do the same:

  1. List the endpoints

    GET /api/v1/rcs/webhooks returns every subscription with its webhook_id, url and active flag.

  2. Read each endpoint's attempts, in parallel

    GET /api/v1/rcs/webhooks/{id}/deliveries?limit=100 for each. This is an N+1, and it is fine: N is the number of endpoints on an account, a handful, not a page of them.

  3. Degrade rather than fail

    Catch per-endpoint errors and substitute an empty list. One endpoint failing should cost you that endpoint's rows, not the whole log.

  4. Merge and sort by sent_at, descending

    The rows carry no cross-endpoint ordering of their own.

text
const { webhooks = [] } = await api("/api/v1/rcs/webhooks");

let partial = false;
const perEndpoint = await Promise.all(
  webhooks.map((w) =>
    api(`/api/v1/rcs/webhooks/${w.webhook_id}/deliveries?limit=100`)
      .then((r) => r.deliveries || [])
      .catch(() => {
        // One endpoint's attempts failing must not blank the whole log.
        partial = true;
        return [];
      }),
  ),
);

const rows = perEndpoint
  .flat()
  .sort((a, b) => (a.sent_at < b.sent_at ? 1 : a.sent_at > b.sent_at ? -1 : 0));

Say so when the result is partial

Two things make this list incomplete: the per-endpoint cap, and any endpoint whose fetch failed. A table that silently implies completeness it does not have is worse than one that admits the limit, surface both, the way the panel does.

Debugging recipes

“You never sent me the event”

Filter attempts by event_type and check whether a row exists. If it does, status_code and response_snippet say what your server replied, which usually settles it.

Intermittent failures

Group attempts by delivery_id. Seeing attempts 1–3 fail and 4 succeed is the retry schedule doing its job; nothing needs fixing. Seeing every attempt time out at ~30000 ms in took_ms is your endpoint being too slow, acknowledge with a 200 immediately and process asynchronously.

Everything failing since a deploy

Check status_code across recent attempts. A wall of 401s or 403s usually means a rotated signing secret that the consumer was not updated for; a wall of 404s means the route moved.

A gap in your data

Read the dead-letter queue for that window. Anything there never arrived, no matter how healthy your logs look. Replay it.