# Webhooks API

> Register endpoints that receive signed event notifications.

Base URL: `https://api.emailit.com/v2`. Authenticate with `Authorization: Bearer <API key>`.

## Create a webhook — POST /webhooks

> Register an HTTP endpoint that receives signed event notifications, choose its event types and, on Pro and above, a payload filter.

# Create a webhook

Creates a webhook endpoint in your workspace. Emailit sends matching events to the URL in batches of up to 100, as a JSON array signed with the webhook's `secret`. See [Webhook requests](/docs/webhooks/webhook-requests/) for the request format. Requires an API key with `full` scope.

`POST /webhooks`

## Request body

- `name` (string, required): Webhook name. Must be unique in the workspace; you can use it instead of the ID in other webhook endpoints.

- `url` (string, required): Endpoint that receives the events. `http` and `https` URLs are accepted; use `https` in production. Emailit resolves the hostname when you save and rejects `localhost`, private, link-local and other reserved IP addresses. Redirects aren't followed when delivering, so use the final URL.

- `all_events` (boolean): Send every event type, including types added later. Default `false`. When `true`, `events` is ignored.

- `enabled` (boolean): Whether Emailit delivers events to the webhook. Default `true`.

- `events` (string[]): Event types to send, for example `["email.delivered", "email.bounced"]`. See [Event types](/docs/webhooks/event-types/). Default `[]`, which with `all_events: false` means the webhook receives nothing. Event names aren't validated. A misspelled type is saved but never matches an event.

- `filter` (object | null): Payload filter. Emailit only sends events whose object matches the rules. Available on Pro, Business and Custom plans; a filter with rules on Pay as you go returns `403`.

- `filter.match` (string): `all` (default) sends an event when every rule matches. `any` sends it when at least one rule matches.

- `filter.rules` (object[]): Up to 25 rules.

- `filter.rules[].field` (string, required): Dotted path into the event object, for example `to`, `status`, `meta.plan` or, for click and open events, `email.campaign.id`. A leading `payload.` is ignored.

- `filter.rules[].operator` (string, required): `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than`, `is_set`, `is_not_set`, `in` or `not_in`. Text operators compare values as strings; `greater_than` and `less_than` compare numbers.

- `filter.rules[].value` (any): Value to compare with. Required for every operator except `is_set` and `is_not_set`. Use an array with `in` and `not_in`.

## Returns

Returns `201 Created` with the webhook object, including the signing `secret` (`whsec_` followed by 64 hex characters). Use the secret to [verify request signatures](/docs/webhooks/request-signature/). You can read it again with [Retrieve a webhook](/docs/api-reference/webhooks/get/) and rotate it with [Rotate the signing secret](/docs/api-reference/webhooks/reset-secret/).

| Status | When |
| --- | --- |
| `400` | `name` or `url` is missing, the URL is invalid, can't be resolved or points to a blocked address, or the filter is invalid. |
| `403` | The filter has rules and your plan doesn't include webhook filters. The body is `{"error": "plan_required", "required_plan": "pro"}`. |
| `409` | A webhook with this name already exists. The body includes the `existing` webhook's `id` and `name`. |
| `422` | The workspace has reached its plan's webhook limit. The body includes `usage.used` and `usage.limit`. See [Limits](/docs/limits/). |

**Request** `POST /webhooks`

**Node.js**

```javascript
import { Emailit } from '@emailit/node';
const emailit = new Emailit('your_api_key');

const webhook = await emailit.webhooks.create({
    name: 'Production events',
    url: 'https://api.acme.com/webhooks/emailit',
    events: ['email.delivered']
});
```

**Python**

```python
from emailit import EmailitClient
client = EmailitClient("your_api_key")

webhook = client.webhooks.create({
    "name": "Production events",
    "url": "https://api.acme.com/webhooks/emailit",
    "events": ["email.delivered"]
})
```

**PHP**

```php
$emailit = Emailit::client('your_api_key');

$webhook = $emailit->webhooks()->create([
    'name' => 'Production events',
    'url' => 'https://api.acme.com/webhooks/emailit',
    'events' => ['email.delivered']
]);
```

**Ruby**

```ruby
require "emailit"
client = Emailit::EmailitClient.new("your_api_key")

webhook = client.webhooks.create(
    name: "Production events",
    url: "https://api.acme.com/webhooks/emailit",
    events: ["email.delivered"]
)
```

**Go**

```go
import "github.com/emailit/emailit-go/v2"
client := emailit.NewClient("your_api_key")

webhook, err := client.Webhooks.Create(&emailit.CreateWebhookRequest{
    Name:   "Production events",
    Url:    "https://api.acme.com/webhooks/emailit",
    Events: []string{"email.delivered"},
})
```

**Rust**

```rust
use emailit::Emailit;
let emailit = Emailit::new("your_api_key");

let webhook = emailit.webhooks.create(
    emailit::types::CreateWebhookParams::new(
        "Production events",
        "https://api.acme.com/webhooks/emailit"
    ).with_events(vec!["email.delivered".into()])
).await?;
```

**Java**

```java
import com.emailit.*;
import com.emailit.params.*;
EmailitClient emailit = new EmailitClient("your_api_key");

EmailitObject webhook = emailit.webhooks().create(
    WebhookCreateParams.builder()
        .setName("Production events")
        .setUrl("https://api.acme.com/webhooks/emailit")
        .setEvents(Arrays.asList("email.delivered"))
        .build()
);
```

**.NET**

```csharp
using Emailit;
var emailit = new EmailitClient("your_api_key");

var webhook = emailit.Webhooks.Create(new WebhookCreateOptions {
    Name = "Production events",
    Url = "https://api.acme.com/webhooks/emailit",
    Events = new[] { "email.delivered" }
});
```

**Laravel**

```php
use Emailit\Laravel\Facades\Emailit;

$webhook = Emailit::webhooks()->create([
    'name' => 'Production events',
    'url' => 'https://api.acme.com/webhooks/emailit',
    'events' => ['email.delivered']
]);
```

**cURL**

```bash
curl -X POST https://api.emailit.com/v2/webhooks \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production events",
    "url": "https://api.acme.com/webhooks/emailit",
    "events": ["email.delivered"]
  }'
```

**201**

```json
{
  "object": "webhook",
  "id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
  "name": "Production events",
  "url": "https://api.acme.com/webhooks/emailit",
  "all_events": false,
  "enabled": true,
  "events": ["email.delivered", "email.bounced"],
  "filter": null,
  "filters_allowed": true,
  "last_used_at": null,
  "created_at": "2026-10-01T09:41:05.302000+00:00",
  "updated_at": "2026-10-01T09:41:05.302000+00:00",
  "secret": "whsec_175a02aca3fb7dc3107ca21e4224a73d058cacc628356a39e020422aec3ca434"
}
```

**400**

```json
{
  "error": "URL resolves to a private/reserved IP address"
}
```

**403**

```json
{
  "error": "plan_required",
  "required_plan": "pro"
}
```

**409**

```json
{
  "error": "Webhook with this name already exists",
  "existing": {
    "object": "webhook",
    "id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
    "name": "Production events"
  }
}
```

**422**

```json
{
  "error": "Pay as you go includes 3 webhook endpoints.",
  "usage": {
    "used": 3,
    "limit": 3
  }
}
```

**Filter**

```json
{
  "name": "Enterprise bounces",
  "url": "https://api.acme.com/webhooks/emailit",
  "events": ["email.bounced", "email.complained"],
  "filter": {
    "match": "all",
    "rules": [
      { "field": "meta.plan", "operator": "equals", "value": "enterprise" },
      { "field": "to", "operator": "not_contains", "value": "@acme.com" }
    ]
  }
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/create/

## Retrieve a webhook — GET /webhooks/{id}

> Retrieve a webhook by its ID or name, including its URL, event types, payload filter and signing secret.

# Retrieve a webhook

Returns one webhook, looked up by ID or by name. This is the only read endpoint that returns the signing `secret`. Requires an API key with `full` scope.

`GET /webhooks/:id`

## Path parameters

- `id` (string, required): Webhook ID (`wh_…`) or the webhook's name, URL-encoded.

## Returns

Returns `200 OK` with the webhook object, including `secret` and `filters_allowed` (whether your plan lets the webhook use a payload filter). `last_used_at` is the time of the last successful delivery, or `null` if nothing has been delivered yet.

Returns `404` with `error: "Webhook not found"` if no webhook matches.

**Request** `GET /webhooks/{id}`

**Node.js**

```javascript
import { Emailit } from '@emailit/node';
const emailit = new Emailit('your_api_key');

const webhook = await emailit.webhooks.get('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e');
```

**Python**

```python
from emailit import EmailitClient
client = EmailitClient("your_api_key")

webhook = client.webhooks.get("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e")
```

**PHP**

```php
$emailit = Emailit::client('your_api_key');

$webhook = $emailit->webhooks()->get('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e');
```

**Ruby**

```ruby
require "emailit"
client = Emailit::EmailitClient.new("your_api_key")

webhook = client.webhooks.get("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e")
```

**Go**

```go
import "github.com/emailit/emailit-go/v2"
client := emailit.NewClient("your_api_key")

webhook, err := client.Webhooks.Get("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e")
```

**Rust**

```rust
use emailit::Emailit;
let emailit = Emailit::new("your_api_key");

let webhook = emailit.webhooks.get("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e").await?;
```

**Java**

```java
import com.emailit.*;
EmailitClient emailit = new EmailitClient("your_api_key");

EmailitObject webhook = emailit.webhooks().get("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e");
```

**.NET**

```csharp
using Emailit;
var emailit = new EmailitClient("your_api_key");

var webhook = emailit.Webhooks.Get("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e");
```

**Laravel**

```php
use Emailit\Laravel\Facades\Emailit;

$webhook = Emailit::webhooks()->get('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e');
```

**cURL**

```bash
curl https://api.emailit.com/v2/webhooks/wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e \
  -H "Authorization: Bearer your_api_key"
```

**200**

```json
{
  "object": "webhook",
  "id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
  "name": "Production events",
  "url": "https://api.acme.com/webhooks/emailit",
  "all_events": false,
  "enabled": true,
  "events": ["email.delivered", "email.bounced"],
  "filter": {
    "match": "all",
    "rules": [
      { "field": "meta.plan", "operator": "equals", "value": "enterprise" }
    ]
  },
  "filters_allowed": true,
  "last_used_at": "2026-10-01T10:02:17.845000+00:00",
  "created_at": "2026-10-01T09:41:05.302000+00:00",
  "updated_at": "2026-10-01T09:41:05.302000+00:00",
  "secret": "whsec_175a02aca3fb7dc3107ca21e4224a73d058cacc628356a39e020422aec3ca434"
}
```

**404**

```json
{
  "error": "Webhook not found"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/get/

## Update a webhook — POST /webhooks/{id}

> Change a webhook's name, URL, event types, payload filter or enabled state. Look it up by its ID or by its name.

# Update a webhook

Updates a webhook. Send only the fields you want to change; at least one is required. The signing secret doesn't change; rotate it with [Rotate the signing secret](/docs/api-reference/webhooks/reset-secret/). Requires an API key with `full` scope.

`POST /webhooks/:id`

## Path parameters

- `id` (string, required): Webhook ID (`wh_…`) or the webhook's name, URL-encoded.

## Request body

- `name` (string): New name. Must be unique in the workspace.

- `url` (string): New endpoint URL, `http` or `https`. Validated the same way as on [create](/docs/api-reference/webhooks/create/).

- `all_events` (boolean): `true` sends every event type and clears the `events` list. If you set it to `false`, also send `events`, or the webhook receives nothing.

- `enabled` (boolean): `false` stops deliveries and `true` resumes them. Events that happen while the webhook is disabled aren't queued for it and aren't sent later.

- `events` (string[]): Replaces the list of event types. Ignored while `all_events` is `true`. Names aren't validated.

- `filter` (object | null): Replaces the payload filter, in the same format as on [create](/docs/api-reference/webhooks/create/). Send `null` to remove it. A filter with rules needs a Pro, Business or Custom plan.

## Returns

Returns `200 OK` with the updated webhook. The `secret` isn't included; use [Retrieve a webhook](/docs/api-reference/webhooks/get/) to read it.

| Status | When |
| --- | --- |
| `400` | The body has none of the fields above, or the URL or filter is invalid. |
| `403` | The filter has rules and your plan doesn't include webhook filters (`plan_required`). |
| `404` | No webhook matches `id`. |
| `409` | Another webhook already uses the new name. |

**Request** `POST /webhooks/{id}`

**Node.js**

```javascript
import { Emailit } from '@emailit/node';
const emailit = new Emailit('your_api_key');

const webhook = await emailit.webhooks.update('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e', {
    enabled: false
});
```

**Python**

```python
from emailit import EmailitClient
client = EmailitClient("your_api_key")

webhook = client.webhooks.update("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e", {
    "enabled": False
})
```

**PHP**

```php
$emailit = Emailit::client('your_api_key');

$webhook = $emailit->webhooks()->update('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e', [
    'enabled' => false
]);
```

**Ruby**

```ruby
require "emailit"
client = Emailit::EmailitClient.new("your_api_key")

webhook = client.webhooks.update("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e", enabled: false)
```

**Go**

```go
import "github.com/emailit/emailit-go/v2"
client := emailit.NewClient("your_api_key")

webhook, err := client.Webhooks.Update("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e", &emailit.UpdateWebhookRequest{
    Enabled: false,
})
```

**Rust**

```rust
use emailit::Emailit;
let emailit = Emailit::new("your_api_key");

let webhook = emailit.webhooks.update("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
    emailit::types::UpdateWebhookParams {
        enabled: Some(false),
        ..Default::default()
    }
).await?;
```

**Java**

```java
import com.emailit.*;
import com.emailit.params.*;
EmailitClient emailit = new EmailitClient("your_api_key");

EmailitObject webhook = emailit.webhooks().update("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
    WebhookUpdateParams.builder()
        .setEnabled(false)
        .build()
);
```

**.NET**

```csharp
using Emailit;
var emailit = new EmailitClient("your_api_key");

var webhook = emailit.Webhooks.Update("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e", new WebhookUpdateOptions {
    Enabled = false
});
```

**Laravel**

```php
use Emailit\Laravel\Facades\Emailit;

$webhook = Emailit::webhooks()->update('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e', [
    'enabled' => false
]);
```

**cURL**

```bash
curl -X POST https://api.emailit.com/v2/webhooks/wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
```

**200**

```json
{
  "object": "webhook",
  "id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
  "name": "Production events",
  "url": "https://api.acme.com/webhooks/emailit",
  "all_events": false,
  "enabled": false,
  "events": ["email.delivered", "email.bounced"],
  "filter": null,
  "filters_allowed": true,
  "last_used_at": "2026-10-01T10:02:17.845000+00:00",
  "created_at": "2026-10-01T09:41:05.302000+00:00",
  "updated_at": "2026-10-01T10:15:40.000000+00:00"
}
```

**400**

```json
{
  "error": "No valid fields provided for update. Provide at least one of: name, url, all_events, enabled, events, filter"
}
```

**404**

```json
{
  "error": "Webhook not found"
}
```

**409**

```json
{
  "error": "Another webhook with this name already exists"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/update/

## List webhooks — GET /webhooks

> List the webhooks in your workspace with search, filters and page-based pagination, plus your plan's webhook usage.

# List webhooks

Returns the webhooks in your workspace, newest first, and how many your plan allows. Signing secrets aren't included in the list. Requires an API key with `full` scope.

`GET /webhooks`

## Query parameters

- `page` (integer): Page number, starting at `1`. Default `1`.

- `limit` (integer): Webhooks per page, from `1` to `100`. Default `10`.

- `search` (string): Case-insensitive match on the webhook name or URL.

- `match`, `order`, `direction`: see [Filtering](https://emailit.com/docs/api-reference/filtering/).

## Filters and sort

Filter and sort keys: see [Filtering](https://emailit.com/docs/api-reference/filtering/).

## Returns

Returns `200 OK` with the webhooks in `data`, `next_page_url` and `previous_page_url` (`null` at either end), and a `usage` object: `used` is the number of webhooks in the workspace, `limit` is your plan's maximum, and `filters_allowed` says whether your plan includes payload filters.

**Request** `GET /webhooks`

**Node.js**

```javascript
import { Emailit } from '@emailit/node';
const emailit = new Emailit('your_api_key');

const webhooks = await emailit.webhooks.list();
```

**Python**

```python
from emailit import EmailitClient
client = EmailitClient("your_api_key")

webhooks = client.webhooks.list()
```

**PHP**

```php
$emailit = Emailit::client('your_api_key');

$webhooks = $emailit->webhooks()->list();
```

**Ruby**

```ruby
require "emailit"
client = Emailit::EmailitClient.new("your_api_key")

webhooks = client.webhooks.list
```

**Go**

```go
import "github.com/emailit/emailit-go/v2"
client := emailit.NewClient("your_api_key")

webhooks, err := client.Webhooks.List(nil)
```

**Rust**

```rust
use emailit::Emailit;
let emailit = Emailit::new("your_api_key");

let webhooks = emailit.webhooks.list(None).await?;
```

**Java**

```java
import com.emailit.*;
EmailitClient emailit = new EmailitClient("your_api_key");

EmailitObject webhooks = emailit.webhooks().list();
```

**.NET**

```csharp
using Emailit;
var emailit = new EmailitClient("your_api_key");

var webhooks = emailit.Webhooks.List();
```

**Laravel**

```php
use Emailit\Laravel\Facades\Emailit;

$webhooks = Emailit::webhooks()->list();
```

**cURL**

```bash
curl https://api.emailit.com/v2/webhooks \
  -H "Authorization: Bearer your_api_key"
```

**200**

```json
{
  "data": [
    {
      "object": "webhook",
      "id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
      "name": "Production events",
      "url": "https://api.acme.com/webhooks/emailit",
      "all_events": false,
      "enabled": true,
      "events": ["email.delivered", "email.bounced"],
      "filter": null,
      "filters_allowed": true,
      "last_used_at": "2026-10-01T10:02:17.845000+00:00",
      "created_at": "2026-10-01T09:41:05.302000+00:00",
      "updated_at": "2026-10-01T10:02:17.845000+00:00"
    }
  ],
  "next_page_url": null,
  "previous_page_url": null,
  "usage": {
    "used": 1,
    "limit": 10,
    "filters_allowed": true
  }
}
```

**401**

```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid API key"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/list/

## Delete a webhook — DELETE /webhooks/{id}

> Delete a webhook by its ID or name. Emailit stops sending events to its URL, including retries that are still pending.

# Delete a webhook

Permanently deletes a webhook and its event subscriptions. To stop deliveries temporarily, [update the webhook](/docs/api-reference/webhooks/update/) with `enabled: false` instead. Requires an API key with `full` scope.

`DELETE /webhooks/:id`

## Path parameters

- `id` (string, required): Webhook ID (`wh_…`) or the webhook's name, URL-encoded.

## Returns

Returns `200 OK` with the deleted webhook's `id` and `name` and `deleted: true`. Returns `404` with `error: "Webhook not found"` if no webhook matches.

**Request** `DELETE /webhooks/{id}`

**Node.js**

```javascript
import { Emailit } from '@emailit/node';
const emailit = new Emailit('your_api_key');

await emailit.webhooks.delete('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e');
```

**Python**

```python
from emailit import EmailitClient
client = EmailitClient("your_api_key")

client.webhooks.delete("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e")
```

**PHP**

```php
$emailit = Emailit::client('your_api_key');

$emailit->webhooks()->delete('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e');
```

**Ruby**

```ruby
require "emailit"
client = Emailit::EmailitClient.new("your_api_key")

client.webhooks.delete("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e")
```

**Go**

```go
import "github.com/emailit/emailit-go/v2"
client := emailit.NewClient("your_api_key")

err := client.Webhooks.Delete("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e")
```

**Rust**

```rust
use emailit::Emailit;
let emailit = Emailit::new("your_api_key");

emailit.webhooks.delete("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e").await?;
```

**Java**

```java
import com.emailit.*;
EmailitClient emailit = new EmailitClient("your_api_key");

emailit.webhooks().delete("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e");
```

**.NET**

```csharp
using Emailit;
var emailit = new EmailitClient("your_api_key");

emailit.Webhooks.Delete("wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e");
```

**Laravel**

```php
use Emailit\Laravel\Facades\Emailit;

Emailit::webhooks()->delete('wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e');
```

**cURL**

```bash
curl -X DELETE https://api.emailit.com/v2/webhooks/wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e \
  -H "Authorization: Bearer your_api_key"
```

**200**

```json
{
  "object": "webhook",
  "id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
  "name": "Production events",
  "deleted": true
}
```

**404**

```json
{
  "error": "Webhook not found"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/delete/

## Send a test event — POST /webhooks/{id}/test

> Send a signed sample event of any type to a webhook endpoint and get the endpoint's response back right away.

# Send a test event

Sends a sample event of the type you choose to the webhook's URL and returns your endpoint's response. Use it to check that your endpoint is reachable and verifies [signatures](/docs/webhooks/request-signature/) correctly. Requires an API key with the `full` scope.

The request has the same format, headers and signature as a real delivery: a JSON array with one event whose `event_id` starts with `evt_test_`, signed with the webhook's current secret. It's sent even if the webhook is disabled or not subscribed to that type, isn't stored as a webhook request, and isn't retried. The sample data is fixed and doesn't refer to real objects.

You can send 5 test events per minute from the same IP address; more return `429`.

`POST /webhooks/{id}/test`

## Path parameters

- `id` (string, required): The webhook ID (`wh_…`) or the webhook's name.

## Body parameters

- `type` (string, required): The event type to send. One of the types below.

| Resource | Event types |
| --- | --- |
| Email | `email.accepted`, `email.scheduled`, `email.delivered`, `email.bounced`, `email.attempted`, `email.failed`, `email.rejected`, `email.clicked`, `email.loaded`, `email.complained`, `email.received`, `email.suppressed`, `email.canceled`, `email.unsubscribed`, `email.resubscribed` |
| Domain | `domain.created`, `domain.updated`, `domain.deleted` |
| Audience | `audience.created`, `audience.updated`, `audience.deleted` |
| Subscriber | `subscriber.created`, `subscriber.updated`, `subscriber.deleted` |
| Contact | `contact.created`, `contact.updated`, `contact.deleted` |
| Template | `template.created`, `template.updated`, `template.deleted` |
| Suppression | `suppression.created`, `suppression.updated`, `suppression.deleted` |
| Email verification | `email_verification.created`, `email_verification.updated`, `email_verification_list.created`, `email_verification_list.updated` |
| Campaign | `campaign.created`, `campaign.updated`, `campaign.deleted`, `campaign.scheduled`, `campaign.queued`, `campaign.sending`, `campaign.testing`, `campaign.sent`, `campaign.canceled`, `campaign.archived` |

See [Event types](/docs/webhooks/event-types/) for what each event means.

## Returns

- `ok` (boolean): `true` if your endpoint answered with a 2xx status.

- `status_code` (integer): Your endpoint's HTTP status. `0` if Emailit couldn't connect, the request timed out after 30 seconds, the endpoint redirected (redirects aren't followed), or the URL points to a blocked address.

- `body` (string): The first 2,000 characters of your endpoint's response, or the connection error.

- `type` (string): The event type sent.

- `payload` (object[]): The exact JSON array that was sent.

Returns `400` if `type` is missing or unknown, `404` if the webhook doesn't exist, and `429` when you exceed the test limit.

**Request** `POST /webhooks/{id}/test`

**cURL**

```bash
curl -X POST https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/test \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "email.delivered" }'
```

**Node.js**

```javascript
const res = await fetch('https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ type: 'email.delivered' }),
});
const { ok, status_code, body } = await res.json();
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/test",
    headers={"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}"},
    json={"type": "email.delivered"},
)
result = r.json()
```

**PHP**

```php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.emailit.com/v2/']);

$response = $client->post('webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/test', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('EMAILIT_API_KEY')],
    'json' => ['type' => 'email.delivered'],
]);
$result = json_decode($response->getBody(), true);
```

**200**

```json
{
  "ok": true,
  "status_code": 200,
  "type": "email.delivered",
  "payload": [
    {
      "event_id": "evt_test_3G0pUekgBm1uIxi9m4C380H70Zq",
      "type": "email.delivered",
      "object": {
        "id": "eml_test_001",
        "email_id": 12345,
        "message_id": "<test-token@mydomain.com>",
        "from": "sender@mydomain.com",
        "to": "recipient@example.com",
        "subject": "Test email",
        "status": "delivered",
        "delivered_at": "2026-01-15T10:30:00.000Z"
      },
      "data": {
        "object": {
          "id": "eml_test_001",
          "email_id": 12345,
          "message_id": "<test-token@mydomain.com>",
          "from": "sender@mydomain.com",
          "to": "recipient@example.com",
          "subject": "Test email",
          "status": "delivered",
          "delivered_at": "2026-01-15T10:30:00.000Z"
        }
      }
    }
  ],
  "body": "{\"received\":true}"
}
```

**400**

```json
{
  "error": "Unknown event type"
}
```

**429**

```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded, retry in 1 minute"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/test/

## Rotate the signing secret — POST /webhooks/{id}/reset-secret

> Replace a webhook's signing secret. Every delivery after the rotation, including retries, is signed with the new secret.

# Rotate the signing secret

Generates a new signing secret for the webhook and returns it. Requires an API key with the `full` scope.

The old secret stops being used immediately: every request sent after the rotation, including retries of earlier events, is signed with the new secret. There's no overlap period, so update the secret in your endpoint right after rotating, or accept both secrets for a short time while you switch. See [Verify request signatures](/docs/webhooks/request-signature/).

`POST /webhooks/{id}/reset-secret`

## Path parameters

- `id` (string, required): The webhook ID (`wh_…`) or the webhook's name.

## Returns

Returns the webhook object with the new `secret` (`whsec_` followed by 64 hexadecimal characters). Returns `404` if the webhook doesn't exist.

**Request** `POST /webhooks/{id}/reset-secret`

**cURL**

```bash
curl -X POST https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/reset-secret \
  -H "Authorization: Bearer $EMAILIT_API_KEY"
```

**Node.js**

```javascript
const res = await fetch('https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/reset-secret', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}` },
});
const { secret } = await res.json();
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/reset-secret",
    headers={"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}"},
)
secret = r.json()["secret"]
```

**PHP**

```php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.emailit.com/v2/']);

$response = $client->post('webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/reset-secret', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('EMAILIT_API_KEY')],
]);
$secret = json_decode($response->getBody(), true)['secret'];
```

**200**

```json
{
  "object": "webhook",
  "id": "wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ",
  "name": "Order notifications",
  "url": "https://acme.com/webhooks/emailit",
  "all_events": false,
  "enabled": true,
  "events": ["email.delivered", "email.bounced", "email.complained"],
  "filter": null,
  "last_used_at": "2026-10-01T12:58:40.000000+00:00",
  "created_at": "2026-08-14T09:12:03.000000+00:00",
  "updated_at": "2026-10-01T13:20:11.000000+00:00",
  "secret": "whsec_a0a593d006bdec5aff2f21e88afd543ba239431acbe7d8dc7a9723d76e0a64e2"
}
```

**404**

```json
{
  "error": "Webhook not found"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/reset-secret/

## Retry failed requests — POST /webhooks/{id}/retry-failed

> Queue every permanently failed webhook request from the last 7 days for delivery again and re-enable the webhook.

# Retry failed requests

Queues every request of this webhook that permanently failed in the last 7 days for delivery again. Requires an API key with the `full` scope.

A request fails permanently after its last automatic retry (11 attempts over several days; see [Retries and failures](/docs/webhooks/retries-and-failures/)). Retried requests start over with a full retry schedule and are delivered within seconds. If at least one request is queued and the webhook was disabled, for example after 3 days of continuous failures, it's enabled again.

Fix your endpoint first, or the requests fail again. To retry a single request, use [Retry one request](/docs/api-reference/webhooks/retry-request/).

`POST /webhooks/{id}/retry-failed`

## Path parameters

- `id` (string, required): The webhook ID (`wh_…`) or the webhook's name.

## Returns

- `retried` (integer): Number of requests queued again. `0` if there was nothing to retry; the webhook's enabled state doesn't change then.

Returns `404` if the webhook doesn't exist.

**Request** `POST /webhooks/{id}/retry-failed`

**cURL**

```bash
curl -X POST https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/retry-failed \
  -H "Authorization: Bearer $EMAILIT_API_KEY"
```

**Node.js**

```javascript
const res = await fetch('https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/retry-failed', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}` },
});
const { retried } = await res.json();
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/retry-failed",
    headers={"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}"},
)
retried = r.json()["retried"]
```

**PHP**

```php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.emailit.com/v2/']);

$response = $client->post('webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/retry-failed', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('EMAILIT_API_KEY')],
]);
$retried = json_decode($response->getBody(), true)['retried'];
```

**200**

```json
{
  "retried": 37
}
```

**404**

```json
{
  "error": "Webhook not found"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/retry-failed/

## Retry one request — POST /webhooks/{id}/requests/{request_id}/retry

> Queue a single permanently failed webhook request for delivery again and re-enable the webhook if it was disabled.

# Retry one request

Queues one permanently failed webhook request for delivery again, with a fresh retry schedule. If the webhook was disabled, it's enabled again. Requires an API key with the `full` scope.

Only requests that have exhausted their automatic retries can be retried this way; requests that are still pending or retrying return `400`. Find request IDs (`whr_…`) on the webhook's **Requests** tab under **Email API → Webhooks**. To retry everything from the last 7 days at once, use [Retry failed requests](/docs/api-reference/webhooks/retry-failed/).

`POST /webhooks/{id}/requests/{request_id}/retry`

## Path parameters

- `id` (string, required): The webhook ID (`wh_…`) or the webhook's name.

- `request_id` (string, required): The webhook request ID (`whr_…`).

## Returns

- `retried` (integer): Always `1`.

- `id` (string): The request ID that was queued.

| Status | When |
| --- | --- |
| `400` | The request hasn't permanently failed, or it has no event to resend. |
| `404` | The webhook doesn't exist, or the request doesn't belong to it. |

**Request** `POST /webhooks/{id}/requests/{request_id}/retry`

**cURL**

```bash
curl -X POST https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/requests/whr_3tWVVMd2eHv3R9B5DWTLtwwU05X/retry \
  -H "Authorization: Bearer $EMAILIT_API_KEY"
```

**Node.js**

```javascript
const res = await fetch(
  'https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/requests/whr_3tWVVMd2eHv3R9B5DWTLtwwU05X/retry',
  { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}` } },
);
const result = await res.json();
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api.emailit.com/v2/webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/requests/whr_3tWVVMd2eHv3R9B5DWTLtwwU05X/retry",
    headers={"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}"},
)
result = r.json()
```

**PHP**

```php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.emailit.com/v2/']);

$response = $client->post('webhooks/wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ/requests/whr_3tWVVMd2eHv3R9B5DWTLtwwU05X/retry', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('EMAILIT_API_KEY')],
]);
$result = json_decode($response->getBody(), true);
```

**200**

```json
{
  "retried": 1,
  "id": "whr_3tWVVMd2eHv3R9B5DWTLtwwU05X"
}
```

**400**

```json
{
  "error": "Only permanently failed requests can be retried"
}
```

**404**

```json
{
  "error": "Webhook request not found"
}
```

---
Source: https://emailit.com/docs/api-reference/webhooks/retry-request/
