Create a webhook
Register an HTTPS endpoint for event delivery.
Register an HTTPS endpoint and choose which event types it receives. At least one event type is required — list every type you want (see the Webhooks page for all eleven).
POST /v3/webhooks
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Required | Destination URL. Must be https — a scheme-less value gets https:// prepended, and an explicit http:// URL is rejected. Private and internal addresses are also rejected. |
events | array of strings | Required | One or more event types to deliver: queued, sent, delivered, opened, clicked, bounced, complained, unsubscribed, delayed, rejected, failed. Unknown values are rejected. |
enabled | boolean | Optional | Create the endpoint paused by passing false. Default true. |
secret | string | Optional | Signing secret, 32–512 chars (a shorter secret is rejected). Omit to have one generated (whsec_…) — it is revealed only in the create response, so store it immediately. |
domain_id | uuid | Optional | Scope the endpoint to a single sending domain you own. Omit (or pass null) to receive events for all domains. |
auth | object | Optional | Receiver-side authentication — credentials wemail presents to your server on every delivery. Defaults to { "type": "none" }. See the fields below. |
The auth object
If your receiver sits behind an auth layer, tell wemail how to authenticate. Pick a type and supply its fields — fixed credentials (bearer, basic, custom_headers) are sent as-is on every delivery; oauth2 makes wemail fetch a client-credentials token from your identity provider and attach it as a Bearer header, cached per endpoint until expiry, refreshed automatically, and retried once with a fresh token on a 401/403. A delivery never goes out with a missing or invalid token — a failed token fetch fails the attempt into the normal retry schedule.
| Parameter | Type | Required | Description |
|---|---|---|---|
auth.type | string | Optional | One of none, hmac, bearer, basic, custom_headers, oauth2, mtls. Default none. Pick a type to reveal its fields. |
auth.secret | string | Optional | hmac only, optional. Omit to have wemail generate a whsec_ signing secret — the plaintext is returned once on the create response, then stored encrypted and never shown again. Reads echo secret_set and a secret_hint (…{last4}). |
auth.header_name | string | Optional | hmac only, optional. Signature header name (default X-Wemail-Signature). Each delivery also carries X-Wemail-Timestamp and X-Wemail-Key-Id. Signature format: t={unix_ts},v1={hex hmac-sha256 of "{t}.{raw_body}"} — reject timestamps older than 5 minutes. |
auth.token | string | Optional | bearer only (required). Delivered as Authorization: Bearer <token>. Write-only — never echoed back. |
auth.username | string | Optional | basic only (required). Delivered as Authorization: Basic <base64(username:password)>. |
auth.password | string | Optional | basic only (required). Write-only — never echoed back. |
auth.headers | object | Optional | custom_headers only (required). Up to 5 name→value pairs merged into every delivery. Names: RFC 7230 tokens, max 64 chars; values: max 1024 chars, no CR/LF. Content-Type and wemail signature headers cannot be overridden. Values are write-only. |
auth.token_url | string (URL) | Optional | oauth2 only (required). Your identity provider's client-credentials token endpoint — must be a public HTTPS URL (private/internal targets are rejected). |
auth.client_id | string | Optional | oauth2 only (required). Sent form-urlencoded with grant_type=client_credentials. |
auth.client_secret | string | Optional | oauth2 only (required). Write-only — never echoed back. |
auth.scope | string | Optional | oauth2 only, optional. Included in the token request when set. |
auth.audience | string | Optional | oauth2 only, optional. Included in the token request when set. |
auth.client_cert | string (PEM) | Optional | mtls only (required). PEM client certificate, max 16 KB. Validated and checked to match the key at save time; reads echo cert_fingerprint_sha256, cert_subject and cert_expires_at. |
auth.client_key | string (PEM) | Optional | mtls only (required). PEM private key, max 16 KB. Write-only — presented during the TLS handshake with your server; reads echo key_set. |
Responses
| Status | Description |
|---|---|
201 | Created. The webhook endpoint. For HMAC auth the generated signing secret is returned once, here, and never again. |
400 | Validation error — e.g. a non-HTTPS url, a signing secret shorter than 32 characters, an unknown domain, or a URL that fails SSRF checks. |
401 | Missing or invalid API key. |
403 | Your plan's webhook-endpoint allowance is used up — upgrade for unlimited endpoints. |
Code examples
curl -X POST https://api.wemail.io/v3/webhooks \
-H "Authorization: Bearer afn_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/wemail",
"events": ["delivered", "bounced", "complained"],
"auth": {
"type": "oauth2",
"token_url": "https://auth.yourapp.com/oauth/token",
"client_id": "wemail-events",
"client_secret": "s3cr3t-value",
"scope": "webhooks:write"
}
}'// Your endpoint sits behind OAuth? Give wemail client credentials —
// it fetches, caches and refreshes the token itself.
const webhook = await wemail.webhooks.create({
url: "https://yourapp.com/webhooks/wemail",
events: ["delivered", "bounced", "complained"],
auth: {
type: "oauth2",
token_url: "https://auth.yourapp.com/oauth/token",
client_id: "wemail-events",
client_secret: process.env.WEBHOOK_OAUTH_SECRET,
scope: "webhooks:write",
},
});webhook = wemail.webhooks.create(
url="https://yourapp.com/webhooks/wemail",
events=["delivered", "bounced", "complained"],
auth={
"type": "oauth2",
"token_url": "https://auth.yourapp.com/oauth/token",
"client_id": "wemail-events",
"client_secret": os.environ["WEBHOOK_OAUTH_SECRET"],
"scope": "webhooks:write",
},
)$webhook = $wemail->webhooks->create([
'url' => 'https://yourapp.com/webhooks/wemail',
'events' => ['delivered', 'bounced', 'complained'],
'auth' => [
'type' => 'oauth2',
'token_url' => 'https://auth.yourapp.com/oauth/token',
'client_id' => 'wemail-events',
'client_secret' => getenv('WEBHOOK_OAUTH_SECRET'),
'scope' => 'webhooks:write',
],
]);webhook = wemail.webhooks.create(
url: "https://yourapp.com/webhooks/wemail",
events: ["delivered", "bounced", "complained"],
auth: {
type: "oauth2",
token_url: "https://auth.yourapp.com/oauth/token",
client_id: "wemail-events",
client_secret: ENV["WEBHOOK_OAUTH_SECRET"],
scope: "webhooks:write"
}
)webhook, _ := client.Webhooks.Create(ctx, &wemail.WebhookParams{
URL: "https://yourapp.com/webhooks/wemail",
Events: []string{"delivered", "bounced", "complained"},
Auth: map[string]interface{}{
"type": "oauth2",
"token_url": "https://auth.yourapp.com/oauth/token",
"client_id": "wemail-events",
"client_secret": os.Getenv("WEBHOOK_OAUTH_SECRET"),
"scope": "webhooks:write",
},
})JSONObject webhook = wemail.webhooks().create(
WebhookParams.builder()
.url("https://yourapp.com/webhooks/wemail")
.events(List.of("delivered", "bounced", "complained"))
.auth(Map.of(
"type", "oauth2",
"token_url", "https://auth.yourapp.com/oauth/token",
"client_id", "wemail-events",
"client_secret", System.getenv("WEBHOOK_OAUTH_SECRET"),
"scope", "webhooks:write"))
.build());var webhook = await wemail.Webhooks.CreateAsync(new WebhookParams {
Url = "https://yourapp.com/webhooks/wemail",
Events = new[] { "delivered", "bounced", "complained" },
Auth = new {
type = "oauth2",
token_url = "https://auth.yourapp.com/oauth/token",
client_id = "wemail-events",
client_secret = Environment.GetEnvironmentVariable("WEBHOOK_OAUTH_SECRET"),
scope = "webhooks:write",
},
});{
"id": "9b2f6c1e-4a8d-4f3b-9c7e-2d1a5b8e0f4c",
"url": "https://yourapp.com/webhooks/wemail",
"events": ["delivered", "bounced", "complained"],
"enabled": true,
"domain_id": null,
"secret": "whsec_1f8a…d92c",
"auth": {
"type": "oauth2",
"token_url": "https://auth.yourapp.com/oauth/token",
"client_id": "wemail-events",
"scope": "webhooks:write",
"client_secret_set": true
},
"created_at": "2026-05-10T09:42:18Z"
}