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

# Webhook security

> Verify X-Robase-Signature as HMAC-SHA256 of the raw body. Prevent replays by checking event ids.

`X-Robase-Signature` is hex-encoded HMAC-SHA256 of the **exact bytes** in the body, keyed with the workspace signing secret (dashboard → Webhooks). Re-parsing and re-serializing JSON will not match. The body is compact (no pretty-print) and does not HTML-escape `<>&` in `data.message`.

Respond `2xx` only after the signature checks out. Reject with `401` or `403` and Robase stops rather than retrying, so a rejected delivery never queues behind your traffic.

## Node.js

```js theme={null}
import crypto from "node:crypto";

app.post("/webhooks/robase", express.raw({ type: "*/*" }), (req, res) => {
  const sig = req.headers["x-robase-signature"];
  const expected = crypto
    .createHmac("sha256", process.env.ROBASE_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body.toString());
  // event.event is "otp.sent", "sms.delivered", ...
  res.json({ received: true });
});
```

## Go

```go theme={null}
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Robase-Signature")

mac := hmac.New(sha256.New, []byte(os.Getenv("ROBASE_WEBHOOK_SECRET")))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))

if !hmac.Equal([]byte(sig), []byte(expected)) {
    http.Error(w, "invalid signature", http.StatusUnauthorized)
    return
}
```

## PHP

```php theme={null}
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_ROBASE_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, getenv('ROBASE_WEBHOOK_SECRET'));

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

Official SDKs also expose signature helpers. Rotate the secret in the dashboard if it leaks; verifiers still using the old secret will fail immediately. Rotation takes effect at once, queued deliveries included — each one is signed when it is sent, not when it is queued.

Treat `data.id` as an idempotency key on your side so a retried delivery is not processed twice.


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