Documentation

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 eventMeaningSendHog event
SMTP receiptAn incoming email was durably stored for an enabled domain. Fetch its body or complete original separately.email.received
SendSES accepted the message for processing. This does not confirm delivery.email.sent
DeliveryThe recipient’s mail server accepted the email. This does not guarantee inbox placement.email.delivered
BounceDelivery failed. Details distinguish permanent and transient bounces and affected recipients.email.bounced
ComplaintThe receiving provider reported a spam complaint or other feedback.email.complained
RejectSES rejected the message content after accepting the request.email.rejected
DeliveryDelayDelivery is delayed; SES may still be trying. This is not a final bounce.email.delayed
Rendering FailureSES could not render an SES-hosted template. SendHog templates render before sending, so their validation errors return directly instead.email.render_failed
OpenA tracking image was requested. A privacy proxy or bot may generate it. Open tracking is not enabled by SendHog.email.opened
ClickA tracked link was requested. This is not proof of a human click. Click tracking is not enabled by SendHog.email.clicked
SubscriptionSES 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

Read email events
Provider

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);
Install SDKnpm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'

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

One webhook for received mail and delivery events
Provider

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);

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

Register a webhook
Provider

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);
Install SDKnpm install 'git+https://github.com/SendHog/sdk-node.git#v0.3.0'

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.

Verify all email events with Standard Webhooks
Provider

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 / ...

SDK verification for the outbound types it recognizes

Verify an incoming webhook
Provider

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.

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.