Register a webhook
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:
{
"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:
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')
})
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)
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)
}
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());
}
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)
);
}
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.
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.