> For the complete documentation index, see [llms.txt](https://docs.robinmesh.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.robinmesh.com/api-reference/webhooks.md).

# Webhooks

Rather than polling the Jobs API, you can have RobinMesh push events to any HTTPS endpoint you control. Typical uses: reacting the moment a job settles, watching for a dwindling credit balance, or kicking off downstream processing.

***

## Configuring a webhook

The Settings tab at `robinmesh.com/app/settings` lets you create and manage webhooks in the browser.

The same operations are available over the API:

```bash
curl -X POST https://api.robinmesh.com/v1/webhooks \
  -H "Authorization: Bearer rmesh_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/robinmesh-events",
    "events": ["job.completed", "credit.low"],
    "secret": "your_signing_secret"
  }'
```

```json
{
  "id": "wh_9ax3mb5n7kqrstvwxyz",
  "url": "https://your-server.com/robinmesh-events",
  "events": ["job.completed", "credit.low"],
  "created_at": "2026-06-15T09:00:00Z",
  "status": "active"
}
```

Deliveries are signed with the `secret`. You see it once, at creation, so put it somewhere safe.

***

## Event types

| Event               | When it fires                                                              |
| ------------------- | -------------------------------------------------------------------------- |
| `job.submitted`     | A job exists and its credits sit in escrow                                 |
| `job.processing`    | A worker picked up the job and inference began                             |
| `job.completed`     | On-chain settlement finished and the worker received payment               |
| `job.failed`        | The job died before finishing (a routing error, or the worker dropped out) |
| `job.timeout`       | 120 seconds passed with no worker finishing; the credits were refunded     |
| `job.disputed`      | Someone contested a completed job                                          |
| `credit.low`        | Your balance fell under the threshold you configured                       |
| `credit.topup`      | Your balance received new credits                                          |
| `worker.registered` | A worker joined the network (of interest to providers)                     |
| `worker.slashed`    | A worker lost stake after a proof was confirmed fraudulent                 |

The `events` array takes any combination, so subscribe narrowly rather than to everything.

***

## Event payload format

Every event arrives wrapped in the same envelope:

```json
{
  "id": "evt_7bx2ma4n6jpqruvwxyz",
  "event": "job.completed",
  "created_at": "2026-06-15T14:22:03Z",
  "api_version": "2026-06-01",
  "data": { ... }
}
```

### `job.completed`

```json
{
  "id": "evt_7bx2ma4n6jpqruvwxyz",
  "event": "job.completed",
  "created_at": "2026-06-15T14:22:03Z",
  "api_version": "2026-06-01",
  "data": {
    "job_id": "job_8fx2kp3m9qrstvwxyz",
    "model": "qwen3-8b",
    "tier": "standard",
    "credits_charged": 8,
    "usdg_value": 0.08,
    "worker_address": "0x9d24ab7e315f68c0d1b2fa4c8e0973d65a1cbe48",
    "settlement_tx": "0x3a91d5c07f26e8b4915dc3a08e67f21b49c0d8a35e7612fb08d94ce5a172b36d",
    "block_number": 12847293,
    "prompt_tokens": 48,
    "completion_tokens": 214,
    "credits_remaining": 1412
  }
}
```

### `credit.low`

```json
{
  "id": "evt_3cx1la5n7kqrtvwxyz",
  "event": "credit.low",
  "created_at": "2026-06-15T15:00:00Z",
  "api_version": "2026-06-01",
  "data": {
    "credits_remaining": 87,
    "usdg_value": 0.87,
    "threshold": 100
  }
}
```

Set the `credit.low` threshold in Settings; it starts at 100 credits.

### `worker.slashed`

```json
{
  "id": "evt_5dx4nb8m2lqruvwxyz",
  "event": "worker.slashed",
  "created_at": "2026-06-15T16:00:00Z",
  "api_version": "2026-06-01",
  "data": {
    "worker_address": "0x9d24ab7e315f68c0d1b2fa4c8e0973d65a1cbe48",
    "slash_amount_rmesh": 500,
    "slash_tx": "0x6d15f8a2c30b97e4d68f012a5c4be79308d1a6f5e2c48b09173da5e6f0b2c481",
    "reason": "fraudulent_proof",
    "related_job_id": "job_2ax1jb4k8npqruvwxyz"
  }
}
```

***

## Verifying webhook signatures

Each delivery carries a `RobinMesh-Signature` header. Checking it proves two things: the payload originated from RobinMesh, and nobody altered it in transit.

**Signature format:**

```
RobinMesh-Signature: t=1750000000,v1=a1b2c3d4e5f6...
```

* `t` holds the delivery time as a Unix timestamp
* `v1` holds an HMAC-SHA256, keyed with your webhook secret, over the string `{timestamp}.{raw_request_body}`

**Verification in Node.js:**

```typescript
import crypto from "crypto"

function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const match = signature.match(/^t=(\d+),v1=([0-9a-f]+)$/)
  if (!match) return false
  const [, timestamp, receivedSig] = match

  const payload = `${timestamp}.${rawBody}`
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex")

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(receivedSig)
  )
}

// Wire it up inside the handler that receives deliveries:
app.post("/robinmesh-events", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["robinmesh-signature"] as string
  const isValid = verifyWebhook(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)

  if (!isValid) {
    return res.status(400).json({ error: "Invalid signature" })
  }

  const event = JSON.parse(req.body.toString())
  // Process the event here.
  res.json({ received: true })
})
```

**Verification in Python:**

```python
import hmac
import hashlib
import re

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]+)", signature)
    if not match:
        return False
    timestamp, received_sig = match.groups()

    payload = f"{timestamp}.{raw_body.decode()}"
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, received_sig)
```

Compare signatures in constant time (`timingSafeEqual` in Node, `hmac.compare_digest` in Python) so an attacker cannot learn the signature byte by byte through timing.

***

## Retry behavior

A delivery counts as failed when your endpoint answers with anything outside 2xx or takes longer than 10 seconds. Failed deliveries are retried on an exponential backoff schedule:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 30 seconds |
| 3       | 5 minutes  |
| 4       | 30 minutes |
| 5       | 2 hours    |

Once the fifth attempt fails, the webhook enters a failed state and retries stop. The Settings tab shows failed deliveries and lets you replay them.

***

## Managing webhooks

List webhooks:

```bash
curl https://api.robinmesh.com/v1/webhooks \
  -H "Authorization: Bearer rmesh_live_your_key_here"
```

Delete a webhook:

```bash
curl -X DELETE https://api.robinmesh.com/v1/webhooks/wh_9ax3mb5n7kqrstvwxyz \
  -H "Authorization: Bearer rmesh_live_your_key_here"
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.robinmesh.com/api-reference/webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
