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

# Webhooks

> Receive real-time delivery events from Torpedo

## Register a webhook

```bash theme={null}
curl -X POST https://api.torpedo.co.mz/api/v1/webhooks \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://myapp.com/webhooks/torpedo",
    "events": ["email.delivered", "email.bounced", "email.complained", "email.failed"]
  }'
```

The response includes a `signingSecret` — **save it immediately**, it is returned once only.

***

## Available events

| Event              | When it fires                                             |
| ------------------ | --------------------------------------------------------- |
| `email.delivered`  | AWS SES confirmed delivery to the recipient's inbox       |
| `email.bounced`    | Email bounced — hard bounces are suppressed automatically |
| `email.complained` | Recipient marked the email as spam                        |
| `email.failed`     | Delivery failed after retries                             |
| `domain.verified`  | Domain DNS records verified successfully                  |
| `domain.failed`    | Domain verification failed                                |

***

## Event payload shape

All events share this envelope:

```json theme={null}
{
  "event": "email.delivered",
  "data": {
    "id": "01960000-0000-0000-0000-000000000000",
    "workspaceId": "...",
    "to": "user@example.com",
    "from": "hello@myapp.com",
    "subject": "Welcome",
    "status": "delivered",
    "deliveredAt": "2026-04-15T10:00:00.000Z"
  }
}
```

***

## Verifying the signature

Every request from Torpedo includes an `X-Signature` header:

```
X-Signature: sha256=a3f2c1d8...
```

Verify it to confirm the request is genuine:

<CodeGroup>
  ```js Node.js theme={null}
  import { createHmac, timingSafeEqual } from 'crypto'

  function verifySignature(rawBody, signature, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret)
  .update(rawBody)
  .digest('hex')
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  }

  // Express example
  app.post('/webhooks/torpedo', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-signature']
  if (!verifySignature(req.body, sig, process.env.TORPEDO_WEBHOOK_SECRET)) {
  return res.status(401).send('Invalid signature')
  }
  const { event, data } = JSON.parse(req.body)
  res.status(200).send('ok')
  })

  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = 'sha256=' + hmac.new(
          secret.encode(), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```

  ```go Go theme={null}
  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"fmt"
  )

  func verifySignature(rawBody []byte, signature, secret string) bool {
  	mac := hmac.New(sha256.New, []byte(secret))
  	mac.Write(rawBody)
  	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
  	return hmac.Equal([]byte(expected), []byte(signature))
  }

  func main() {
  	ok := verifySignature([]byte(`{"event":"email.delivered",...}`),
  		"sha256=abc123...", "your_signing_secret")
  	fmt.Println("valid:", ok)
  }
  ```

  ```java Java theme={null}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.util.HexFormat;

  public static boolean verifySignature(byte[] rawBody, String signature, String secret) throws Exception {
      Mac mac = Mac.getInstance("HmacSHA256");
      mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
      String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(rawBody));
      // Constant-time comparison
      return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
  }
  ```

  ```csharp C# theme={null}
  using System.Security.Cryptography;
  using System.Text;

  static bool VerifySignature(byte[] rawBody, string signature, string secret)
  {
      using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
      var hash = hmac.ComputeHash(rawBody);
      var expected = "sha256=" + Convert.ToHexString(hash).ToLower();
      return CryptographicOperations.FixedTimeEquals(
          Encoding.UTF8.GetBytes(expected),
          Encoding.UTF8.GetBytes(signature)
      );
  }
  ```
</CodeGroup>

<Warning>
  Always verify the signature before processing webhook payloads. Use timing-safe comparison
  (`timingSafeEqual`, `hmac.Equal`, `CryptographicOperations.FixedTimeEquals`) to prevent timing
  attacks — never use plain string equality.
</Warning>

***

## Delivery retries

Torpedo retries failed webhook deliveries with exponential backoff. Your endpoint should:

* Return `2xx` within 5 seconds
* Be idempotent — the same event may be delivered more than once

View delivery history via `GET /api/v1/webhooks/{id}/deliveries`.
