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

> ## Agent Instructions
> For chatbots and coding agents, start at /guides/ai-agents.
> Hosted MCP: POST https://api.robase.dev/mcp (Streamable HTTP). Auth: Authorization: Bearer robe_… or robe_agent_….
> Phase 1 tools: get_pricing, send_otp, verify_otp, get_otp, send_sms, get_sms. No payment or top-up tools.
> Call get_pricing; treat listed:true as marketed markets (NG, GH, KE, ZA, GB, BJ, CI, US). Do not invent countries or delivery percentages.
> Prefer OTP-shaped tests such as "Your Robase test OTP is 123456". Do not send Hello or Good morning as tests. Match errors on error.type.

# Webhooks

> Dashboard-configured HTTPS callbacks. HMAC-SHA256 over the raw body.

Configure one endpoint per workspace in the [dashboard](https://robase.dev/app/webhooks). There is no public webhook CRUD API.

Each delivery is `POST` with:

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `X-Robase-Signature` | hex HMAC-SHA256 of the **raw** body |
| `X-Robase-Event` | event name |
| `User-Agent` | `Robase-Webhook/1.0` |

Verify against the bytes you received, not a re-encoded JSON object. The signing secret is shown and rotatable on the webhooks page.

## Payload

The signed body is **compact JSON** (no extra spaces, no trailing newline). `json.Marshal` pretty-print or a parser that re-encodes the object will not match `X-Robase-Signature`. `data` field order follows the structs below (`id`, `phone_number`, `country_code`, …). `timestamp` is UTC RFC3339 with whole seconds.

OTP events:

```json theme={null}
{"event":"otp.sent","timestamp":"2026-08-29T10:20:00Z","data":{"id":"550e8400-e29b-41d4-a716-446655440000","phone_number":"+2348012345678","country_code":"NG","status":"sent","credit_cost":1}}
```

`otp.delivered` uses `"status":"delivered"`. `otp.failed` adds `"reason"` (a stable token) and `"refunded"` (boolean) when known.

SMS events include `message`. `<`, `>`, and `&` in the body are **not** HTML-escaped:

```json theme={null}
{"event":"sms.failed","timestamp":"2026-08-29T10:20:00Z","data":{"id":"550e8400-e29b-41d4-a716-446655440000","phone_number":"+2348012345678","country_code":"NG","message":"Pay <https://example.com?a=1&b=2>","status":"failed","credit_cost":1,"reason":"delivery_failed","refunded":true}}
```

`sms.blocked` also sets `data.antispam` (`is_spam`, `score`, `category`, `reason`).

Credit events:

```json theme={null}
{"event":"credit_balance.low","timestamp":"2026-08-29T10:20:00Z","data":{"workspace_id":"550e8400-e29b-41d4-a716-446655440000","balance":9,"threshold":10}}
```

`credit_balance.topped_up` omits `threshold`. Dashboard test-fire sets `"test":true` on credit samples so you can ignore them. Production credit events omit `test`.

## Events

From `internal/core/webhook.go` (also the dashboard picker):

| Event | When |
| - | - |
| `otp.sent` | OTP queued / accepted upstream |
| `otp.delivered` | Carrier DLR says delivered |
| `otp.verified` | Code accepted |
| `otp.expired` | TTL elapsed without a valid verify |
| `otp.failed` | Every configured route failed, or the carrier reported a failure (credit refunded) |
| `sms.sent` | Transactional SMS accepted upstream |
| `sms.delivered` | Carrier DLR says delivered |
| `sms.failed` | Every configured route failed, or the carrier reported a failure (credit refunded) |
| `sms.blocked` | Anti-spam stopped the message before dispatch (credit refunded) |
| `credit_balance.low` | Balance dropped past 50, 25 or 10 credits, or your own threshold |
| `credit_balance.exhausted` | Balance reached zero |
| `credit_balance.topped_up` | Credits landed after a successful charge — interactive checkout or auto top-up |

`otp.failed`, `sms.failed` and `sms.blocked` also carry `reason` (a stable token) and `refunded` (a boolean read from the credit ledger) in `data` when they are known. See [Delivery timeline](/sms/delivery-timeline) for the tokens.

Dashboard test-fire for `credit_balance.*` uses the same `data` shape (`workspace_id`, `balance`, optional `threshold`) and sets `"test": true` so you can ignore samples.

Respond with `2xx`. A timeout, a `408`, a `429` or any `5xx` is retried with backoff, up to 3 attempts. Every other `4xx` reads as a refusal and is not retried — fix the endpoint, then retry the delivery from the dashboard.

## When an endpoint keeps failing

The webhooks page grades your endpoint on the last 24 hours of deliveries and counts the failures in a row. Every attempt in the delivery log carries the exact JSON we signed, the `X-Robase-Signature` we sent with it, and your endpoint's own reply — enough to debug a rejection without adding logging on your side.

While deliveries are failing, we email the workspace's owners, admins and finance members every six hours with the count and a link to the log. After 20 failures in a row we switch the endpoint off and email you at once: events stop being delivered so the queue does not fill with events nobody receives. Turn it back on from the webhooks page once the endpoint is fixed — the failure count resets and deliveries resume from the next event. Events that fired while it was off are not replayed.

See [Webhook security](/guides/webhook-security) for verification code.


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