Skip to content

Webhooks

A webhook is an address on your server that Azimea calls when something happens to one of your emails: it was delivered, it bounced, the person opened it or clicked a link. Each notification is signed, so your server can prove it comes from Azimea before acting on it.

Only the owner and admins of the company can do this.

  1. In the dashboard, open Settings › API keys and scroll to Webhooks.
  2. Click Add webhook.
  3. In Address, enter the URL on your server that receives the notifications. It must start with https://.
  4. Under Events, tick the events you want. Leave all of them unticked to receive every event, including the ones added later.
  5. Click Add webhook. The signing secret (it starts with whsec_) appears once, above the list: copy it now and keep it with your server’s secrets. Azimea never shows it again; if you lose it, delete the webhook and add it again.
  6. Click Send test next to the new webhook. Azimea sends a webhook.test notification right away and tells you whether your server answered with a 2xx.

A company can have up to 20 active webhooks. To stop one, click Delete: notifications stop at once, including those still waiting to be retried.

Event When it is sent
email.sent The email left Azimea for the sending server. It does not mean it arrived yet.
email.delivered The recipient’s mail server received it.
email.bounced The recipient’s mail server refused it for good. data.reason says why.
email.complained The person marked it as spam.
email.opened The email was opened for the first time.
email.clicked A link in it was clicked for the first time. data.url is the link.
webhook.test A test, asked for to check that your address answers.

A webhook can listen to some of these events or, with an empty list, to all of them. Each event is sent at most once per email: the first open and the first click, not every one. An open or a click may come from a mail privacy feature or a link scanner rather than a person.

A POST with a JSON body and three headers:

POST /azimea HTTP/1.1
Content-Type: application/json
webhook-id: 01a10912-5b3c-7d41-9e2f-6b8c1d0a4f77
webhook-timestamp: 1791198664
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"id":"01a10912-5b3c-7d41-9e2f-6b8c1d0a4f77","type":"email.delivered","created_at":"2026-10-05T09:31:04.1180000+00:00","data":{"message_id":"01a10900-0000-7000-8000-000000000301","email":"ana@example.com"}}
Field What it is
id The notification’s id, the same as the webhook-id header. Use it to ignore a notification you already handled.
type The event, from the table above.
created_at When it happened, ISO 8601 in UTC.
data.message_id The email: the id you got back from POST /sending/emails, or the id of a campaign or flow email.
data.email The recipient’s address.
data.contact_id The contact in Azimea, when the email went to a contact (campaigns and flows).
data.source_id The campaign or flow that sent it, when there is one.
data.url email.clicked only: the link.
data.reason email.bounced, email.complained and webhook.test: the detail Azimea received.

Fields without a value for the event are left out, not sent empty. The names are in snake_case.

Answer with any 2xx status within 10 seconds, then do the work. Anything else — another status, a timeout, a connection that cannot be made — counts as a failure, and the notification is sent again with the same body and the same webhook-id:

Attempt When
1 Right away
2 1 minute after the first failure
3 5 minutes later
4 15 minutes later
5 1 hour later
6 3 hours later, the last one

After 15 failed deliveries in a row, the webhook is stopped: the dashboard shows it as Stopped, with the reason. Fix your server, then delete the webhook and add it again. Redirects are not followed, and addresses on private networks are refused: give the final public https address.

Because of retries, a notification can arrive more than once: keep the webhook-ids you handled and skip repeats. Notifications about the same email are not guaranteed to arrive in order.

The scheme is Standard Webhooks, so any Standard Webhooks library verifies it with your secret. By hand:

  1. Read the raw body, exactly as it arrived — before any JSON parsing, which would change spaces and order.
  2. Refuse the notification if webhook-timestamp (Unix seconds) is more than 5 minutes away from now, in the past or the future: it is a replay.
  3. Your secret looks like whsec_ followed by base64. The signing key is the part after whsec_, decoded from base64.
  4. Compute HMAC-SHA256 with that key over {webhook-id}.{webhook-timestamp}.{raw body}, and encode it in base64.
  5. webhook-signature holds one or more signatures separated by spaces, each written v1,<base64>. Accept the notification if one of them equals yours, compared in constant time.
using System.Security.Cryptography;
using System.Text;
static bool IsFromAzimea(string secret, string id, string timestamp, string signatures, string rawBody)
{
if (!long.TryParse(timestamp, out var sentAt)
|| Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sentAt) > 300)
{
return false;
}
var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret["whsec_".Length..] : secret);
var expected = HMACSHA256.HashData(key, Encoding.UTF8.GetBytes($"{id}.{timestamp}.{rawBody}"));
foreach (var candidate in signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
if (!candidate.StartsWith("v1,"))
{
continue;
}
var given = new byte[candidate.Length];
if (Convert.TryFromBase64String(candidate[3..], given, out var written)
&& CryptographicOperations.FixedTimeEquals(expected, given.AsSpan(0, written)))
{
return true;
}
}
return false;
}
// ASP.NET Core: read the body as text before anything parses it.
app.MapPost("/azimea", async (HttpRequest request) =>
{
var rawBody = await new StreamReader(request.Body).ReadToEndAsync();
var valid = IsFromAzimea(
Environment.GetEnvironmentVariable("AZIMEA_WEBHOOK_SECRET")!,
request.Headers["webhook-id"].ToString(),
request.Headers["webhook-timestamp"].ToString(),
request.Headers["webhook-signature"].ToString(),
rawBody);
return valid ? Results.Ok() : Results.Unauthorized();
});

The secret is shown once, when the webhook is created. Keep it with your other secrets, never in client-side code.

When you click Send test, Azimea sends it a webhook.test notification, signed like any other, to confirm that the address answers and that your verification accepts it. It carries a made-up message_id and the address proba@azimea.com: answer 2xx and otherwise ignore it.

Emails sent from a sandbox never reach anyone, but they are marked sent and delivered, and their email.sent and email.delivered notifications are sent like real ones: an integration can be tested end to end with an azm_test_ key.