# Webhooks

A webhook sends a POST request to your server when something happens to an email or a contact. Use it to update your own records without polling the API.

## Add an Endpoint

Open Webhooks and click Add Endpoint. Enter a name, an HTTPS URL and the events to send. Your server must answer with a 2xx status within 10 seconds. Send Test Event on the endpoint page checks your setup.

## Event Types

| Event                  | Sent when                               |
| ---------------------- | --------------------------------------- |
| email.sent             | The email leaves Inboxjot.              |
| email.delivered        | The receiving server accepts the email. |
| email.delivery_delayed | Delivery is delayed and will retry.     |
| email.opened           | The recipient opens the email.          |
| email.clicked          | The recipient clicks a link.            |
| email.bounced          | The email bounces.                      |
| email.complained       | The recipient marks the email as spam.  |
| email.rejected         | The email is rejected before delivery.  |
| email.failed           | The email cannot be sent.               |
| email.unsubscribed     | The recipient unsubscribes.             |
| contact.created        | A contact is created.                   |
| contact.updated        | A contact changes.                      |
| contact.deleted        | A contact is deleted.                   |

## Payload

Every request has a JSON body with the event id, type, time and data. Email events can also carry `tags`, the clicked `link` and bounce or complaint details.

```json JSON
{
  "id": "evt_…",
  "type": "email.delivered",
  "created_at": "2026-10-17T09:30:00.000Z",
  "data": {
    "email_id": "eml_…",
    "to": ["riti@example.com"],
    "from": "Acme <hello@send.acme.dev>",
    "subject": "Your receipt from Acme"
  }
}
```

## Verify Signatures

Requests follow the Standard Webhooks spec. Each one has the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Sign `id.timestamp.body` with HMAC-SHA256 and the signing secret, then compare the result with the signature.

```javascript Node.js
import { verifyWebhook } from 'inboxjot';

export async function POST(request) {
  const event = await verifyWebhook({
    payload: await request.text(),
    headers: request.headers,
    secret: process.env.INBOXJOT_WEBHOOK_SECRET,
  });

  if (event.type === 'email.bounced') {
    console.log(event.data.email_id, event.data.bounce.type);
  }

  return new Response(null, { status: 204 });
}
```

```rust Rust
use inboxjot::webhooks::{self, WebhookEventData};

let event = webhooks::verify(&secret, &headers, &body)?;

if let WebhookEventData::EmailBounced(data) = event.data {
    println!("bounced {}: {:?}", data.email.email_id, data.bounce.kind);
}
```

```go Go
func handle(w http.ResponseWriter, r *http.Request) {
	payload, _ := io.ReadAll(r.Body)
	event, err := webhook.Verify(payload, r.Header, os.Getenv("INBOXJOT_WEBHOOK_SECRET"))
	if err != nil {
		http.Error(w, "invalid signature", http.StatusBadRequest)
		return
	}

	if event.Type == webhook.EmailBounced {
		log.Println(event.Email.EmailID, event.Email.Bounce.Type)
	}
	w.WriteHeader(http.StatusNoContent)
}
```

```python Python
import os

from inboxjot import verify_webhook

event = verify_webhook(
    payload=body,
    headers=headers,
    secret=os.environ["INBOXJOT_WEBHOOK_SECRET"],
)

if event["type"] == "email.bounced":
    print(event["data"]["email_id"], event["data"]["bounce"]["type"])
```

> Use the raw request body. Parsing and encoding the JSON again changes the
> bytes and breaks the signature.

## Retries

A failed delivery retries 5 times, after 1 minute, 10 minutes, 1 hour, 6 hours and 16 hours. The webhook-id stays the same, so you can skip events you already handled.

If the last retry fails and the endpoint had no successful delivery in the last 24 hours, Inboxjot turns the endpoint off. The Event Log lists every delivery, and Replay Event sends one again.
