Webhooks and events
Use one signed endpoint for received emails, deliveries, bounces, complaints and other email events.
Email delivery continues after your application sends a message. Amazon SES reports what happens later, such as a recipient’s mail server accepting the message or an address bouncing. SendHog uses signed provider notifications to update message status and enforce sending protections.
Read event history through the API or register one public HTTPS endpoint for incoming emails and delivery notifications. Select email.received alongside email.bounced, email.delivered and other events. Your sending-only API key cannot register webhooks: create a key with webhooks.manage permission.
Use SDK0.3.0 for unified incoming-email and delivery subscriptions and verification; see the installation guide. Older SDK validators do not recognize email.received. Native HTTP registration and the Standard Webhooks verifier below also work without a SendHog SDK.
Does send wait for SES?
Yes. sendhog.send() waits for SES to accept or reject the email, then saves the acceptance before returning the message ID. This also applies to template sends. It is not an immediate background-queue response.
Acceptance is not delivery. A successful response means SES accepted the message for processing; a later Delivery event means the recipient’s mail server accepted it. Neither guarantees that the message appears in the inbox.
There is no public queue flag today. If the provider request has an ambiguous outcome, SendHog reports SendOutcomeUnknown rather than automatically risking a duplicate send. Inspect the message or contact support before trying again.
Email event reference
email.received comes from SendHog’s SMTP receiving pipeline. The other ten types are recognized when SES emits them. Not every type applies to every message: tracking, SES-hosted templates and SES contact lists require their respective features. This release does not enable tracking or fabricate events for unsupported features.
| SES event | Meaning | SendHog event |
|---|---|---|
SMTP receipt | An incoming email was durably stored for an enabled domain. Fetch its body or complete original separately. | email.received |
Send | SES accepted the message for processing. This does not confirm delivery. | email.sent |
Delivery | The recipient’s mail server accepted the email. This does not guarantee inbox placement. | email.delivered |
Bounce | Delivery failed. Details distinguish permanent and transient bounces and affected recipients. | email.bounced |
Complaint | The receiving provider reported a spam complaint or other feedback. | email.complained |
Reject | SES rejected the message content after accepting the request. | email.rejected |
DeliveryDelay | Delivery is delayed; SES may still be trying. This is not a final bounce. | email.delayed |
Rendering Failure | SES could not render an SES-hosted template. SendHog templates render before sending, so their validation errors return directly instead. | email.render_failed |
Open | A tracking image was requested. A privacy proxy or bot may generate it. Open tracking is not enabled by SendHog. | email.opened |
Click | A tracked link was requested. This is not proof of a human click. Click tracking is not enabled by SendHog. | email.clicked |
Subscription | SES contact-list or topic preferences changed. SendHog does not currently expose SES contact-list management. | email.subscription_changed |
Source: Amazon SES event data reference. Events can arrive late, more than once or in a different order. A later delivery update must not clear a complaint or a sending protection.
Read the current message status
Use sendhog.messages.get(id) in JavaScript or sendhog.messages.get(id) in Python. Direct HTTP uses GET /v1/messages/{id}. The API key needs email.read permission for that domain; a sending-only key cannot read messages.
The returned status is a summary, not a complete event history or an outcome for every recipient. Possible saved states include accepted, delivered, bounced, complained and failed. Check the dashboard email log for operational review.
A bounced or complained address may be blocked from future sends. Do not retry suppressed recipients or bypass a disabled domain. See message states and the API reference.
Read individual events
This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendHog } from '@sendhog/sdk';
const client = new SendHog({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendHog } from '@sendhog/sdk';
const client = new SendHog({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog import SendHog
client = SendHog(os.environ['SENDHOG_API_KEY'])
# Requires email.read. Use a SendHog message ID.
page = client.messages.events('MESSAGE_ID', {'limit': 20})
for event in page['data']:
print(event['type'], event['data'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires email.read.
curl 'https://app.sendhog.com/v1/messages/MESSAGE_ID/events?limit=20' \
-H "Authorization: Bearer $SENDHOG_API_KEY"# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SESv2Client } from '@sendhog/sdk/ses';
const client = new SESv2Client({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SESv2Client } from '@sendhog/sdk/ses';
const client = new SESv2Client({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.ses import SESv2Client
client = SESv2Client(os.environ['SENDHOG_API_KEY'])
# Requires email.read. Use a SendHog message ID.
page = client.messages.events('MESSAGE_ID', {'limit': 20})
for event in page['data']:
print(event['type'], event['data'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires email.read.
curl 'https://app.sendhog.com/v1/messages/MESSAGE_ID/events?limit=20' \
-H "Authorization: Bearer $SENDHOG_API_KEY"# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { ServerClient } from '@sendhog/sdk/postmark';
const client = new ServerClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { ServerClient } from '@sendhog/sdk/postmark';
const client = new ServerClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.postmark import ServerClient
client = ServerClient(os.environ['SENDHOG_API_KEY'])
# Requires email.read. Use a SendHog message ID.
page = client.messages.events('MESSAGE_ID', {'limit': 20})
for event in page['data']:
print(event['type'], event['data'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires email.read.
curl 'https://app.sendhog.com/v1/messages/MESSAGE_ID/events?limit=20' \
-H "Authorization: Bearer $SENDHOG_API_KEY"# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { Resend } from '@sendhog/sdk/resend';
const client = new Resend({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { Resend } from '@sendhog/sdk/resend';
const client = new Resend({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.resend import Resend
client = Resend(os.environ['SENDHOG_API_KEY'])
# Requires email.read. Use a SendHog message ID.
page = client.messages.events('MESSAGE_ID', {'limit': 20})
for event in page['data']:
print(event['type'], event['data'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires email.read.
curl 'https://app.sendhog.com/v1/messages/MESSAGE_ID/events?limit=20' \
-H "Authorization: Bearer $SENDHOG_API_KEY"# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendGridClient } from '@sendhog/sdk/sendgrid';
const client = new SendGridClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendGridClient } from '@sendhog/sdk/sendgrid';
const client = new SendGridClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires email.read. Use a SendHog message ID.
const page = await client.messages.events('MESSAGE_ID', { limit: 20 });
for (const event of page.data) console.log(event.type, event.data);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.sendgrid import SendGridAPIClient
client = SendGridAPIClient(os.environ['SENDHOG_API_KEY'])
# Requires email.read. Use a SendHog message ID.
page = client.messages.events('MESSAGE_ID', {'limit': 20})
for event in page['data']:
print(event['type'], event['data'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires email.read.
curl 'https://app.sendhog.com/v1/messages/MESSAGE_ID/events?limit=20' \
-H "Authorization: Bearer $SENDHOG_API_KEY"# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.Call sendhog.messages.events(id, { limit: 20 }) in JavaScript or sendhog.messages.events(id, {"limit": 20}) in Python. HTTP uses GET /v1/messages/{id}/events. Requires email.read.
The result contains data, has_more and next_cursor. Pass the cursor as after for the next page. Events are ordered by when SendHog observed them, newest first, and retained for30days. History begins with this feature’s release; earlier notifications are not reconstructed.
Each event contains id, type, created_at and data. The data identifies message_id, transport provider_id, affected recipients and event-specific details, such as bounce subtype or SMTP diagnostics. In email.received, the provider reference is SendHog’s SMTP receipt ID, and details contain sender, subject, an optional RFC Message-ID, authentication signals and an authenticated raw-download path. Credentials, message bodies, attachment bytes, AWS account identifiers and internal tags are excluded. Oversized provider details may be marked truncated.
Register one endpoint for inbound and delivery events
Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP; SDK0.3.0 also supports unified subscriptions.
import os, json, urllib.request
# Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
request = urllib.request.Request(
'https://app.sendhog.com/v1/webhooks',
data=json.dumps({"url":"https://example.com/webhooks/sendhog","events":["email.received","email.delivered","email.bounced","email.complained"]}).encode(),
headers={'Authorization': 'Bearer ' + os.environ['SENDHOG_API_KEY'], 'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
# Save result['secret'] securely; returned only once. Never log it.
print(result['webhook']['id'])One native SendHog endpoint receives inbound mail, bounces and delivery events.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
}'Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP; SDK0.3.0 also supports unified subscriptions.
import os, json, urllib.request
# Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
request = urllib.request.Request(
'https://app.sendhog.com/v1/webhooks',
data=json.dumps({"url":"https://example.com/webhooks/sendhog","events":["email.received","email.delivered","email.bounced","email.complained"]}).encode(),
headers={'Authorization': 'Bearer ' + os.environ['SENDHOG_API_KEY'], 'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
# Save result['secret'] securely; returned only once. Never log it.
print(result['webhook']['id'])One native SendHog endpoint receives inbound mail, bounces and delivery events.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
}'Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP; SDK0.3.0 also supports unified subscriptions.
import os, json, urllib.request
# Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
request = urllib.request.Request(
'https://app.sendhog.com/v1/webhooks',
data=json.dumps({"url":"https://example.com/webhooks/sendhog","events":["email.received","email.delivered","email.bounced","email.complained"]}).encode(),
headers={'Authorization': 'Bearer ' + os.environ['SENDHOG_API_KEY'], 'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
# Save result['secret'] securely; returned only once. Never log it.
print(result['webhook']['id'])One native SendHog endpoint receives inbound mail, bounces and delivery events.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
}'Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP; SDK0.3.0 also supports unified subscriptions.
import os, json, urllib.request
# Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
request = urllib.request.Request(
'https://app.sendhog.com/v1/webhooks',
data=json.dumps({"url":"https://example.com/webhooks/sendhog","events":["email.received","email.delivered","email.bounced","email.complained"]}).encode(),
headers={'Authorization': 'Bearer ' + os.environ['SENDHOG_API_KEY'], 'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
# Save result['secret'] securely; returned only once. Never log it.
print(result['webhook']['id'])One native SendHog endpoint receives inbound mail, bounces and delivery events.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
}'Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP for the unified event stream. SDK0.3.0 also supports email.received; older SDK validators require an upgrade. No provider preference is changed.
// Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
const response = await fetch('https://app.sendhog.com/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.SENDHOG_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
})
});
if (!response.ok) throw new Error('Webhook registration failed: ' + response.status);
const { webhook, secret } = await response.json();
// Save secret securely; returned only once. Never log it.
console.log(webhook.id);Uses native SendHog HTTP; SDK0.3.0 also supports unified subscriptions.
import os, json, urllib.request
# Native SendHog HTTP. Requires webhooks.manage; no SDK needed.
request = urllib.request.Request(
'https://app.sendhog.com/v1/webhooks',
data=json.dumps({"url":"https://example.com/webhooks/sendhog","events":["email.received","email.delivered","email.bounced","email.complained"]}).encode(),
headers={'Authorization': 'Bearer ' + os.environ['SENDHOG_API_KEY'], 'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
# Save result['secret'] securely; returned only once. Never log it.
print(result['webhook']['id'])One native SendHog endpoint receives inbound mail, bounces and delivery events.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.received",
"email.delivered",
"email.bounced",
"email.complained"
]
}'Enable inbound mail first using the receiving setup guide. Existing registrations are not automatically subscribed to new event types. One endpoint may select all eleven types.
Outbound-only SDK registration
This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendHog } from '@sendhog/sdk';
const client = new SendHog({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendHog } from '@sendhog/sdk';
const client = new SendHog({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog import SendHog
client = SendHog(os.environ['SENDHOG_API_KEY'])
# Requires a key with webhooks.manage scope.
result = client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
})
# Store result['secret'] securely; it is shown only once. Do not log it.
print(result['webhook']['id'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires webhooks.manage. Save the returned signing secret securely.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
}'# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SESv2Client } from '@sendhog/sdk/ses';
const client = new SESv2Client({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SESv2Client } from '@sendhog/sdk/ses';
const client = new SESv2Client({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.ses import SESv2Client
client = SESv2Client(os.environ['SENDHOG_API_KEY'])
# Requires a key with webhooks.manage scope.
result = client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
})
# Store result['secret'] securely; it is shown only once. Do not log it.
print(result['webhook']['id'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires webhooks.manage. Save the returned signing secret securely.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
}'# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { ServerClient } from '@sendhog/sdk/postmark';
const client = new ServerClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { ServerClient } from '@sendhog/sdk/postmark';
const client = new ServerClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.postmark import ServerClient
client = ServerClient(os.environ['SENDHOG_API_KEY'])
# Requires a key with webhooks.manage scope.
result = client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
})
# Store result['secret'] securely; it is shown only once. Do not log it.
print(result['webhook']['id'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires webhooks.manage. Save the returned signing secret securely.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
}'# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { Resend } from '@sendhog/sdk/resend';
const client = new Resend({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { Resend } from '@sendhog/sdk/resend';
const client = new Resend({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.resend import Resend
client = Resend(os.environ['SENDHOG_API_KEY'])
# Requires a key with webhooks.manage scope.
result = client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
})
# Store result['secret'] securely; it is shown only once. Do not log it.
print(result['webhook']['id'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires webhooks.manage. Save the returned signing secret securely.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
}'# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendGridClient } from '@sendhog/sdk/sendgrid';
const client = new SendGridClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import { SendGridClient } from '@sendhog/sdk/sendgrid';
const client = new SendGridClient({ apiKey: process.env.SENDHOG_API_KEY ?? '' });
// Requires a key with webhooks.manage scope.
const { webhook, secret } = await client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
});
// Store secret securely; it is shown only once. Do not log it.
console.log(webhook.id);npm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'This is a SendHog extension shared by all SDK styles, not the selected provider's webhook or event API. Requires SDK 0.2.0+.
import os
from sendhog.sendgrid import SendGridAPIClient
client = SendGridAPIClient(os.environ['SENDHOG_API_KEY'])
# Requires a key with webhooks.manage scope.
result = client.webhooks.create({
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
})
# Store result['secret'] securely; it is shown only once. Do not log it.
print(result['webhook']['id'])python -m pip install 'git+https://github.com/SendHog/sdk-python.git@v0.3.0'Direct SendHog HTTP; requires webhooks.manage. Save the returned signing secret securely.
curl 'https://app.sendhog.com/v1/webhooks' \
-H "Authorization: Bearer $SENDHOG_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/sendhog",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
}'# No SDK installation is required for cURL.
# Use your SendHog API key with direct HTTP requests.HTTP uses POST /v1/webhooks with url and events. Use public HTTPS on port443; local/private addresses, credentials, fragments, redirects and custom ports are not allowed. DNS is checked at registration and every delivery. Up to10 active endpoints and20 registrations per domain per day are allowed.
webhooks.list() lists metadata without secrets. webhooks.delete(id) erases the secret and cancels pending deliveries. HTTP uses GET /v1/webhooks and DELETE /v1/webhooks/{id}. Deletion cannot recall an HTTP request already in flight.
Verify and acknowledge notifications
SendHog uses Standard Webhooks HMAC-SHA256 signatures with webhook-id, webhook-timestamp and webhook-signature headers. Verify against the exact raw request body and the endpoint’s signing secret before trusting JSON.
Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody, headers, signingSecret) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized);
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody: string, headers: HeadersInit, signingSecret: string) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized) as {id: string; type: string; data: Record<string, unknown>};
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks verification supports inbound and delivery events together.
# Install: python -m pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
def verify_incoming(raw_body, headers, signing_secret):
# Pass the exact raw request payload; do not reserialize parsed JSON.
normalized = {key.lower(): value for key, value in headers.items()}
event = Webhook(signing_secret.removeprefix('whsec_')).verify(raw_body, normalized)
if event['id'] != normalized['webhook-id']:
raise ValueError('Event ID mismatch')
return event
# Catch verification failures and reject. Deduplicate event['id'], persist work,
# then return 2xx. Branch on event['type'] in the same handler for all email events.HMAC verification runs inside your receiving server.
# Verify incoming requests in your server, not with cURL.
# Select JavaScript, TypeScript or Python for Standard Webhooks verification.Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody, headers, signingSecret) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized);
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody: string, headers: HeadersInit, signingSecret: string) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized) as {id: string; type: string; data: Record<string, unknown>};
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks verification supports inbound and delivery events together.
# Install: python -m pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
def verify_incoming(raw_body, headers, signing_secret):
# Pass the exact raw request payload; do not reserialize parsed JSON.
normalized = {key.lower(): value for key, value in headers.items()}
event = Webhook(signing_secret.removeprefix('whsec_')).verify(raw_body, normalized)
if event['id'] != normalized['webhook-id']:
raise ValueError('Event ID mismatch')
return event
# Catch verification failures and reject. Deduplicate event['id'], persist work,
# then return 2xx. Branch on event['type'] in the same handler for all email events.HMAC verification runs inside your receiving server.
# Verify incoming requests in your server, not with cURL.
# Select JavaScript, TypeScript or Python for Standard Webhooks verification.Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody, headers, signingSecret) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized);
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody: string, headers: HeadersInit, signingSecret: string) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized) as {id: string; type: string; data: Record<string, unknown>};
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks verification supports inbound and delivery events together.
# Install: python -m pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
def verify_incoming(raw_body, headers, signing_secret):
# Pass the exact raw request payload; do not reserialize parsed JSON.
normalized = {key.lower(): value for key, value in headers.items()}
event = Webhook(signing_secret.removeprefix('whsec_')).verify(raw_body, normalized)
if event['id'] != normalized['webhook-id']:
raise ValueError('Event ID mismatch')
return event
# Catch verification failures and reject. Deduplicate event['id'], persist work,
# then return 2xx. Branch on event['type'] in the same handler for all email events.HMAC verification runs inside your receiving server.
# Verify incoming requests in your server, not with cURL.
# Select JavaScript, TypeScript or Python for Standard Webhooks verification.Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody, headers, signingSecret) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized);
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody: string, headers: HeadersInit, signingSecret: string) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized) as {id: string; type: string; data: Record<string, unknown>};
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks verification supports inbound and delivery events together.
# Install: python -m pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
def verify_incoming(raw_body, headers, signing_secret):
# Pass the exact raw request payload; do not reserialize parsed JSON.
normalized = {key.lower(): value for key, value in headers.items()}
event = Webhook(signing_secret.removeprefix('whsec_')).verify(raw_body, normalized)
if event['id'] != normalized['webhook-id']:
raise ValueError('Event ID mismatch')
return event
# Catch verification failures and reject. Deduplicate event['id'], persist work,
# then return 2xx. Branch on event['type'] in the same handler for all email events.HMAC verification runs inside your receiving server.
# Verify incoming requests in your server, not with cURL.
# Select JavaScript, TypeScript or Python for Standard Webhooks verification.Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody, headers, signingSecret) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized);
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks library verification supports all event types. Older SendHog SDK0.2 helpers reject email.received until upgraded.
// Install: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
// Works for email.received and every delivery event, without old SDK enum limits.
export function verifyIncoming(rawBody: string, headers: HeadersInit, signingSecret: string) {
const normalized = Object.fromEntries(new Headers(headers));
const event = new Webhook(signingSecret.replace(/^whsec_/, '')).verify(rawBody, normalized) as {id: string; type: string; data: Record<string, unknown>};
if (event.id !== normalized['webhook-id']) throw new Error('Event ID mismatch');
return event;
}
// Verify exact raw bytes BEFORE trusting JSON. Catch errors and reject.
// Deduplicate event.id, durably enqueue processing, then return 2xx.
// Branch on event.type: email.received / email.bounced / email.delivered / ...Standard Webhooks verification supports inbound and delivery events together.
# Install: python -m pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
def verify_incoming(raw_body, headers, signing_secret):
# Pass the exact raw request payload; do not reserialize parsed JSON.
normalized = {key.lower(): value for key, value in headers.items()}
event = Webhook(signing_secret.removeprefix('whsec_')).verify(raw_body, normalized)
if event['id'] != normalized['webhook-id']:
raise ValueError('Event ID mismatch')
return event
# Catch verification failures and reject. Deduplicate event['id'], persist work,
# then return 2xx. Branch on event['type'] in the same handler for all email events.HMAC verification runs inside your receiving server.
# Verify incoming requests in your server, not with cURL.
# Select JavaScript, TypeScript or Python for Standard Webhooks verification.SDK verification for the outbound types it recognizes
Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody, headers, signingSecret) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody: string, headers: Headers, signingSecret: string) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
from sendhog import verify_webhook
# Call inside your receiving HTTP handler, before parsing JSON.
def verify_incoming(raw_body, headers, signing_secret):
return verify_webhook(raw_body, headers, signing_secret)
# Deduplicate event['id'] and persist processing before returning 2xx.Signature verification runs on your receiving server. cURL has no equivalent; select a server language.
# cURL cannot verify an incoming webhook on your server.
# Select JavaScript, TypeScript or Python to use the verification helper.
# There is no remote SendHog signature-verification endpoint.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody, headers, signingSecret) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody: string, headers: Headers, signingSecret: string) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
from sendhog import verify_webhook
# Call inside your receiving HTTP handler, before parsing JSON.
def verify_incoming(raw_body, headers, signing_secret):
return verify_webhook(raw_body, headers, signing_secret)
# Deduplicate event['id'] and persist processing before returning 2xx.Signature verification runs on your receiving server. cURL has no equivalent; select a server language.
# cURL cannot verify an incoming webhook on your server.
# Select JavaScript, TypeScript or Python to use the verification helper.
# There is no remote SendHog signature-verification endpoint.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody, headers, signingSecret) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody: string, headers: Headers, signingSecret: string) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
from sendhog import verify_webhook
# Call inside your receiving HTTP handler, before parsing JSON.
def verify_incoming(raw_body, headers, signing_secret):
return verify_webhook(raw_body, headers, signing_secret)
# Deduplicate event['id'] and persist processing before returning 2xx.Signature verification runs on your receiving server. cURL has no equivalent; select a server language.
# cURL cannot verify an incoming webhook on your server.
# Select JavaScript, TypeScript or Python to use the verification helper.
# There is no remote SendHog signature-verification endpoint.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody, headers, signingSecret) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody: string, headers: Headers, signingSecret: string) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
from sendhog import verify_webhook
# Call inside your receiving HTTP handler, before parsing JSON.
def verify_incoming(raw_body, headers, signing_secret):
return verify_webhook(raw_body, headers, signing_secret)
# Deduplicate event['id'] and persist processing before returning 2xx.Signature verification runs on your receiving server. cURL has no equivalent; select a server language.
# cURL cannot verify an incoming webhook on your server.
# Select JavaScript, TypeScript or Python to use the verification helper.
# There is no remote SendHog signature-verification endpoint.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody, headers, signingSecret) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
import { verifyWebhook } from '@sendhog/sdk';
// Call inside your receiving HTTP handler, before parsing JSON.
export async function verifyIncoming(rawBody: string, headers: Headers, signingSecret: string) {
return await verifyWebhook(rawBody, headers, signingSecret);
}
// Deduplicate the returned event.id and persist processing before returning 2xx.Verify SendHog signatures with the same shared helper for every SDK style.
from sendhog import verify_webhook
# Call inside your receiving HTTP handler, before parsing JSON.
def verify_incoming(raw_body, headers, signing_secret):
return verify_webhook(raw_body, headers, signing_secret)
# Deduplicate event['id'] and persist processing before returning 2xx.Signature verification runs on your receiving server. cURL has no equivalent; select a server language.
# cURL cannot verify an incoming webhook on your server.
# Select JavaScript, TypeScript or Python to use the verification helper.
# There is no remote SendHog signature-verification endpoint.The helpers reject invalid signatures, mismatched event IDs and timestamps outside a five-minute window. Keep server clocks accurate. Catch verification errors and return a non-2xx response; do not process an unverified event. Parsed and reserialized JSON is not the original signed body.
Respond with2xx promptly after accepting an event into your own reliable processing. Deduplicate by event ID and tolerate out-of-order arrival. The same event ID/body is retained across retries, with a fresh timestamp/signature on each attempt. There is no automatic exactly-once guarantee.
Delivery attempts and retries
A minute worker processes saved webhook deliveries. Normal first delivery may wait up to the next minute plus backlog time. Requests have a 10-second HTTP timeout. A 2xx response acknowledges delivery; network errors, 408, 429 and 5xx responses are retried after approximately 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours, with jitter. Other non-2xx responses, including redirects, fail permanently; 410 disables the endpoint.
There are at most seven attempts. Notification delivery records are retained for seven days; event history is retained for30days. Email delivery and webhook delivery are separate operations: retrying a webhook never resends the email. A public manual replay API is not available.
Do not connect an existing provider’s handler unchanged or use SendHog’s internal SNS endpoint as a customer webhook. Matching hard bounces and complaints still enforce suppressions and domain protections independently of whether your endpoint acknowledges their events.