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

# Webhooks

No need to keep polling my Jobs API. I'll push events to any HTTPS endpoint you control, so you can react the second a job settles, notice when your units are running low, or start your downstream processing.

***

## Setting up a webhook

You can create and manage webhooks in your browser from my Settings tab at `theacompute.com/app/settings`.

Or do all of the same things through my API:

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

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

I sign every delivery with your `secret`. I only show it to you once, when you create the webhook, so keep it somewhere safe.

***

## Events I can send you

| Event               | When I send it                                                                                            |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| `job.submitted`     | I've created the job and its units are in escrow                                                          |
| `job.processing`    | A worker has taken the job and started inference                                                          |
| `job.completed`     | I've finished on-chain settlement and paid the worker                                                     |
| `job.failed`        | The job stopped before it could finish (a routing error, or the worker dropped out)                       |
| `job.timeout`       | 120 seconds went by without a worker finishing, so I returned the units                                   |
| `job.disputed`      | Someone challenged a completed job                                                                        |
| `credit.low`        | Your balance of units fell below the threshold you set (my API calls units credits, hence the event name) |
| `credit.topup`      | I added new units to your balance                                                                         |
| `worker.registered` | A new worker joined my network (useful if you're a provider)                                              |
| `worker.slashed`    | I took stake from a worker after confirming one of its proofs was fraudulent                              |

You can put any mix of these in the `events` array. Pick only the ones you need rather than subscribing to all of them.

***

## What my event payloads look like

I wrap every event 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
  }
}
```

You can change the `credit.low` threshold in Settings. I set it to 100 units by default.

### `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_thea": 500,
    "slash_tx": "0x6d15f8a2c30b97e4d68f012a5c4be79308d1a6f5e2c48b09173da5e6f0b2c481",
    "reason": "fraudulent_proof",
    "related_job_id": "job_2ax1jb4k8npqruvwxyz"
  }
}
```

***

## Checking my webhook signatures

I put a `TheaCompute-Signature` header on every delivery. Once you verify it, you know two things: the payload really came from me, and nobody changed it on the way to you.

**How my signature is formatted:**

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

* `t` is the moment I delivered the event, as a Unix timestamp
* `v1` is the HMAC-SHA256 I compute over the string `{timestamp}.{raw_request_body}`, using your webhook secret as the key

**Verifying it 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("/theacompute-events", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["theacompute-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 })
})
```

**Verifying it 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)
```

Please check my signature with a constant-time comparison (`timingSafeEqual` in Node, `hmac.compare_digest` in Python). An ordinary string comparison lets an attacker recover the signature one byte at a time just by timing your responses.

***

## How I retry

I count a delivery as failed if your endpoint replies with anything other than 2xx or takes more than 10 seconds. When that happens, I try again on an exponential backoff schedule:

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

If the fifth attempt also fails, I put the webhook in a failed state and stop retrying. You can see failed deliveries in the Settings tab and replay them from there.

***

## Managing your webhooks

To list your webhooks:

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

To delete a webhook:

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