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

# Webhooks

Skip polling the Jobs API. RobinCompute pushes events to any HTTPS endpoint you control: react the moment a job settles, catch a shrinking credit balance, or kick off downstream processing.

***

## Configuring a webhook

Create and manage webhooks in the browser from the Settings tab at `robincompute.org/app/settings`.

The same operations work over the API:

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

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

Deliveries are signed with the `secret`. It appears once, at creation. Store 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 got paid                       |
| `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 came back         |
| `job.disputed`      | Someone contested a completed job                                          |
| `credit.low`        | Your balance dropped 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. Subscribe narrowly, not to everything.

***

## Event payload format

Every event arrives 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_rcompute": 500,
    "slash_tx": "0x6d15f8a2c30b97e4d68f012a5c4be79308d1a6f5e2c48b09173da5e6f0b2c481",
    "reason": "fraudulent_proof",
    "related_job_id": "job_2ax1jb4k8npqruvwxyz"
  }
}
```

***

## Verifying webhook signatures

Each delivery carries a `RobinCompute-Signature` header. Verify it and you know two things: RobinCompute sent the payload, and nobody altered it in transit.

**Signature format:**

```
RobinCompute-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("/robincompute-events", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["robincompute-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). A naive comparison leaks the signature byte by byte through timing.

***

## Retry behavior

A delivery fails when your endpoint answers with anything outside 2xx or takes longer than 10 seconds. Failed deliveries retry on an exponential backoff schedule:

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

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

***

## Managing webhooks

List webhooks:

```bash
curl https://api.robincompute.org/v1/webhooks \
  -H "Authorization: Bearer rcompute_live_your_key_here"
```

Delete a webhook:

```bash
curl -X DELETE https://api.robincompute.org/v1/webhooks/wh_9ax3mb5n7kqrstvwxyz \
  -H "Authorization: Bearer rcompute_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.robincompute.org/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.
