Skip to main content
Documentation

Signed webhooks

Receive events in real time on your server, with HMAC SHA-256 validation against forgeries.

Available events

Receive events in real time on your server, with HMAC SHA-256 validation against forgeries.

FieldTypeDescription
email.senteventEmail accepted by the MTA and handed off to the destination provider.
email.deliveredeventDestination provider confirmed delivery to the inbox.
email.bouncedeventPermanent bounce — recipient does not exist or provider policy.
email.complainedeventRecipient marked the email as spam. Add to suppression automatically.
email.openedeventEmail opened by recipient (pixel tracking).
email.clickedeventLink clicked (tracking).
email.failedeventDefinitive failure after retries.
email.suppressedeventEmail blocked by the suppression list.
email.delivery_delayedeventDelivery delayed (greylisting / destination rate limit).

1. Register the endpoint

Provide a valid HTTPS URL and the list of events you want to receive. We strongly recommend registering a secret.

curl
curl -X POST https://api.coffeemail.com.br/v1/product/webhooks \  -H "Authorization: Bearer $COFFEEMAIL_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "url": "https://sua-api.com.br/webhooks/coffeemail",    "events": ["email.delivered", "email.bounced", "email.complained"],    "secret": "whsec_meusegredocompartilhado"  }'

2. Validate the HMAC signature

Each request arrives with the X-CoffeeMail-Signature header containing HMAC SHA-256 of the raw body. Use coffeemail.webhooks.verifySignature() in your handler.

handler.ts
import { Webhook } from '@coffeemail/node';import http from 'node:http'; const wh = new Webhook(process.env.COFFEEMAIL_WEBHOOK_SECRET!); http.createServer(async (req, res) => {  const signature = req.headers['x-coffeemail-signature'] as string;  const raw = await readBody(req);   try {    wh.verify(raw, signature);  } catch (err) {    res.statusCode = 401;    return res.end('Invalid signature');  }   const event = JSON.parse(raw.toString('utf-8'));  res.statusCode = 202;  res.end('OK');}).listen(3000);

3. Respond in < 5s

Respond 200 OK quickly. If you need to process asynchronously, enqueue locally and return 202. Slow workloads trigger exponential retries.

4. Implement idempotency

Each payload includes a unique event id. Persist it in your database to discard duplicates. Use upsert by id.

idempotency.ts
const event = JSON.parse(raw.toString('utf-8'));const eventId = event.id; await db.processedEvents.upsert({  where: { id: eventId },  create: {    id: eventId,    type: event.type,    payload: event,    receivedAt: new Date(),  },  update: {},});

5. Test the endpoint

Use POST /v1/product/webhooks/:id/test to fire a simulated event. The endpoint receives a fictitious payload with a valid signature.

test-webhook.ts
const { data } = await coffeemail.webhooks.test('wh_123');console.log('Status recebido:', data.statusCode);
Always use tls: true in your handler. Plain HTTP addresses (port 80) are not accepted — CoffeeMail requires valid HTTPS with a trusted certificate.