AhaSend Webhooks
Version 2.0.0
AhaSend webhook events documentation. This specification describes all webhook events that AhaSend sends to your configured webhook URLs. ## Overview Webhooks are HTTP callbacks that AhaSend sends to your configured URLs when specific events occur. They provide real-time notifications about: - **Message Events**: Email delivery status (sent, delivered, bounced, opened, etc.) - **Suppression Events**: When email addresses are automatically suppressed - **Domain Events**: DNS configuration issues - **Route Events**: Inbound email processing ## Standard Webhooks Compatibility AhaSend webhook deliveries use the Standard Webhooks header names, signed-content format, HMAC-SHA256 algorithm, and signature format. Secret handling is intentionally different from the encoded-secret convention assumed by some Standard Webhooks libraries, so compatibility with stock libraries is not unconditional. ### Security Headers All webhooks include these security headers for verification: - `webhook-id`: Unique identifier for the webhook event (used as idempotency key) - `webhook-timestamp`: Unix timestamp when the webhook was sent - `webhook-signature`: HMAC-SHA256 signature of the payload using the resource secret ### Verification The HMAC key is the literal UTF-8 bytes of the `secret` returned when the webhook or route resource is created. Use the complete returned string, including any prefix. Do not Base64-decode it and do not strip a prefix. Use the AhaSend SDK verifier, or a verifier that explicitly accepts raw key bytes. A stock Standard Webhooks library is compatible only if it has a raw-secret/raw-key mode that preserves these literal UTF-8 bytes. Constructors that decode an encoded Standard Webhooks secret will derive a different key and reject valid AhaSend deliveries. The signed content is the unmodified `webhook-id`, `webhook-timestamp`, and raw request body joined with periods. See the [Standard Webhooks verification algorithm](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md#verifying-webhook-authenticity) for the shared protocol details, subject to the AhaSend secret-handling boundary above. ### Retry Policy - Failed webhooks are retried **6 times** over **16+ minutes** - Only HTTP status codes **200-299** are considered successful - After **100 consecutive failures**, the webhook is automatically disabled - You'll receive an email notification when a webhook is disabled ### Payload Format All webhooks follow the Standard Webhooks payload structure: ```json { "type": "event.type", "webhook_id": "abe11757-2886-4b55-96f1-0e0afc95795a", "timestamp": "2024-05-06T09:49:16.687031577Z", "data": { // Event-specific data } } ``` Message, suppression, and domain webhooks use `webhook_id`. Route webhooks use `route_id` instead. ## Getting Started 1. **Configure a webhook** in your AhaSend dashboard 2. **Choose which events** you want to receive 3. **Verify webhook signatures** using the literal resource secret as described above 4. **Handle the events** in your application For more information, visit the [AhaSend webhook documentation](https://ahasend.com/help/integrations/webhooks).
Servers
https://your-webhook-endpoint.com
AsyncAPI Specification
openapi: 3.1.0
info:
title: AhaSend Webhooks
description: |
AhaSend webhook events documentation. This specification describes all webhook events that AhaSend sends to your configured webhook URLs.
## Overview
Webhooks are HTTP callbacks that AhaSend sends to your configured URLs when specific events occur. They provide real-time notifications about:
- **Message Events**: Email delivery status (sent, delivered, bounced, opened, etc.)
- **Suppression Events**: When email addresses are automatically suppressed
- **Domain Events**: DNS configuration issues
- **Route Events**: Inbound email processing
## Standard Webhooks Compatibility
AhaSend webhook deliveries use the Standard Webhooks header names, signed-content format, HMAC-SHA256 algorithm, and signature format. Secret handling is intentionally different from the encoded-secret convention assumed by some Standard Webhooks libraries, so compatibility with stock libraries is not unconditional.
### Security Headers
All webhooks include these security headers for verification:
- `webhook-id`: Unique identifier for the webhook event (used as idempotency key)
- `webhook-timestamp`: Unix timestamp when the webhook was sent
- `webhook-signature`: HMAC-SHA256 signature of the payload using the resource secret
### Verification
The HMAC key is the literal UTF-8 bytes of the `secret` returned when the webhook or route resource is created. Use the complete returned string, including any prefix. Do not Base64-decode it and do not strip a prefix.
Use the AhaSend SDK verifier, or a verifier that explicitly accepts raw key bytes. A stock Standard Webhooks library is compatible only if it has a raw-secret/raw-key mode that preserves these literal UTF-8 bytes. Constructors that decode an encoded Standard Webhooks secret will derive a different key and reject valid AhaSend deliveries.
The signed content is the unmodified `webhook-id`, `webhook-timestamp`, and raw request body joined with periods. See the [Standard Webhooks verification algorithm](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md#verifying-webhook-authenticity) for the shared protocol details, subject to the AhaSend secret-handling boundary above.
### Retry Policy
- Failed webhooks are retried **6 times** over **16+ minutes**
- Only HTTP status codes **200-299** are considered successful
- After **100 consecutive failures**, the webhook is automatically disabled
- You'll receive an email notification when a webhook is disabled
### Payload Format
All webhooks follow the Standard Webhooks payload structure:
```json
{
"type": "event.type",
"webhook_id": "abe11757-2886-4b55-96f1-0e0afc95795a",
"timestamp": "2024-05-06T09:49:16.687031577Z",
"data": {
// Event-specific data
}
}
```
Message, suppression, and domain webhooks use `webhook_id`. Route
webhooks use `route_id` instead.
## Getting Started
1. **Configure a webhook** in your AhaSend dashboard
2. **Choose which events** you want to receive
3. **Verify webhook signatures** using the literal resource secret as described above
4. **Handle the events** in your application
For more information, visit the [AhaSend webhook documentation](https://ahasend.com/help/integrations/webhooks).
version: "2.0.0"
contact:
email: support@ahasend.com
license:
name: Proprietary
servers:
- url: https://your-webhook-endpoint.com
description: Your webhook endpoint URL (configured in AhaSend dashboard)
webhooks:
# Message Event Webhooks
message.reception:
post:
summary: Message Reception Event
description: |
Triggered when an email has been received and queued.
This is always the first event in a message's lifecycle. The message has been accepted by AhaSend and is queued for delivery to the recipient's mail server.
**When it's sent**: Immediately after AhaSend receives and queues an email for delivery.
<Note>
This webhook is called for both inbound and outbound emails.
</Note>
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageReceptionWebhookPayload'
example:
type: "message.reception"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T09:49:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_reception"
from: "sender@example.com"
recipient: "recipient@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.delivered:
post:
summary: Message Delivered Event
description: |
Triggered when an email has been successfully delivered to the recipient's mail server.
This indicates that the recipient's mail server has accepted the email. Note that this doesn't guarantee the email reached the recipient's inbox (it could still be filtered to spam).
**When it's sent**: When the recipient's mail server confirms successful delivery.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageDeliveredWebhookPayload'
example:
type: "message.delivered"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T09:50:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_delivered"
from: "sender@example.com"
recipient: "recipient@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
delivery_attempt:
smtp_code: 250
enhanced_status_code: "2.0.0"
response: "OK: queued"
command: "DATA"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.transient_error:
post:
summary: Message Transient Error Event
description: |
Triggered when an email delivery is delayed due to a temporary issue with the receiving mail server.
This is a temporary failure (soft bounce). AhaSend will retry delivery according to its retry policy. Common causes include:
- Recipient's mailbox is full
- Temporary server issues
- Rate limiting by the recipient's server
**When it's sent**: When a temporary delivery failure occurs.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageTransientErrorWebhookPayload'
example:
type: "message.transient_error"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T09:51:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_transient_error"
from: "sender@example.com"
recipient: "recipient@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
delivery_attempt:
classification: "QuotaIssues"
smtp_code: 452
enhanced_status_code: "4.2.2"
response: "The recipient's inbox is out of storage space"
command: "RCPT TO"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.failed:
post:
summary: Message Failed Event
description: |
Triggered when an email exhausts its retry window without being delivered.
No more delivery attempts will be made. This event reports retry exhaustion rather than one specific SMTP exchange, so `data.delivery_attempt` is omitted. An immediate permanent rejection from a recipient server is reported as `message.bounced` instead.
**When it's sent**: After all retry attempts have been exhausted.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageFailedWebhookPayload'
example:
type: "message.failed"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T09:55:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_failed"
from: "sender@example.com"
recipient: "invalid@nonexistent-domain.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.bounced:
post:
summary: Message Bounced Event
description: |
Triggered when a message reaches a bounced outcome. This includes an immediate permanent SMTP rejection or an out-of-band delivery status notification received after the message was accepted.
A direct SMTP rejection normally includes `data.delivery_attempt`. An out-of-band bounce does not describe one SMTP attempt, so the object is omitted in that case.
**When it's sent**: When a recipient server permanently rejects the message or AhaSend receives a later bounce notification.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageBouncedWebhookPayload'
example:
type: "message.bounced"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T10:05:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_bounced"
from: "sender@example.com"
recipient: "recipient@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
delivery_attempt:
classification: "InvalidRecipient"
smtp_code: 550
enhanced_status_code: "5.1.1"
response: "The email account that you tried to reach does not exist"
command: "RCPT TO"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.suppressed:
post:
summary: Message Suppressed Event
description: |
Triggered when no delivery attempt was made because the recipient is suppressed.
The email was not sent because the recipient email address is on your suppression list. This prevents sending to addresses that have previously bounced or been marked as problematic.
**When it's sent**: When an email is submitted for a suppressed recipient.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageSuppressedWebhookPayload'
example:
type: "message.suppressed"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T10:10:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_suppressed"
from: "sender@example.com"
recipient: "bounced@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.opened:
post:
summary: Message Opened Event
description: |
Triggered when the recipient opens your email.
**Requirements**:
- Open tracking must be enabled for your account or this specific email
- The email must contain HTML content
- The recipient's email client must load images
**Additional Data**: This webhook includes user agent and IP address information for analytics.
**Bot Detection**: The `is_bot` field indicates whether the open was detected as automated (email security scanners, etc.).
**When it's sent**: When the tracking pixel in the email is loaded.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageOpenedWebhookPayload'
example:
type: "message.opened"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T10:15:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_opened"
from: "sender@example.com"
recipient: "recipient@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
ip: "192.168.1.100"
is_bot: false
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
message.clicked:
post:
summary: Message Clicked Event
description: |
Triggered when the recipient clicks a tracked link in your email.
**Requirements**:
- Click tracking must be enabled for your account or this specific email
- The clicked link must be processed by AhaSend's click tracking system
**Additional Data**: This webhook includes user agent and IP address information for analytics.
**When it's sent**: When a tracked link in the email is clicked.
tags:
- Message Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageClickedWebhookPayload'
example:
type: "message.clicked"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T10:20:16.687031577Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
event: "on_clicked"
from: "sender@example.com"
recipient: "recipient@example.com"
subject: "Welcome to our service"
message_id_header: "<message-id-12345@localhost>"
id: "407926766d2711f09b30960002cafe7c"
url: "https://example.com/landing-page"
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
ip: "192.168.1.100"
is_bot: false
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
# Suppression Event Webhooks
suppression.created:
post:
summary: Suppression Created Event
description: |
Triggered when a suppression is created for an email address.
**Why suppressions are created**:
- Multiple hard bounces from the same email address
- Repeated soft bounces over time
- Manual suppression through the API or dashboard
**Auto-expiration**: Suppressions automatically expire after 30 days to allow for temporary issues to be resolved.
**Impact**: Future emails to this address will be automatically suppressed until the suppression expires or is manually removed.
**When it's sent**: When a new suppression is added to your account.
tags:
- Suppression Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SuppressionWebhookPayload'
example:
type: "suppression.created"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T12:57:06.451529527Z"
data:
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
recipient: "bounced@example.com"
created_at: "2024-05-06T12:57:06.451529617Z"
expires_at: "2024-06-05T12:57:06.451529617Z"
reason: "Too many hard bounces"
sending_domain: "your-domain.com"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
# Domain Event Webhooks
domain.dns_error:
post:
summary: Domain DNS Error Event
description: |
AhaSend automatically checks your domain's DNS configuration every 15 to 30 minutes and sends this webhook if any severer issues are detected that will prevent the system from sending emails on your domain's behalf.
Triggered when DNS configuration issues are detected for any domain in your account.
**Common DNS issues**:
- Missing or invalid SPF record
- Missing or invalid DKIM record
- Missing or invalid DMARC record
- DNS propagation delays
**Impact**: After this event, email sending will be rejected until DNS configuration is fixed.
**Resolution**: Check your domain's DNS settings in the AhaSend dashboard and update your DNS records as needed.
**When it's sent**: When AhaSend's automated DNS checker detects configuration problems.
tags:
- Domain Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DomainWebhookPayload'
example:
type: "domain.dns_error"
webhook_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T12:59:46.404433272Z"
data:
domain: "example.com"
account_id: "4cdd7bdd-294e-4762-892f-83d40abf5a87"
spf_valid: false
dkim_valid: false
dmarc_valid: false
dns_last_checked_at: "2024-05-06T12:59:46.404433312Z"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
# Route Event Webhooks (Inbound Email Processing)
message.routing:
post:
summary: Inbound Message Route Event
description: |
Triggered when an inbound email is received and processed through a configured route.
**Purpose**: This webhook delivers complete inbound email data to your application for processing inbound emails (like support tickets, replies, etc.).
**Difference from message events**:
- **Route events**: Process *inbound* emails sent TO your domains
- **Message events**: Track *outbound* emails sent FROM your domains
**Route configuration**:
- Configure routes in your AhaSend dashboard
- Choose whether to include attachments and headers
- Set recipient filters and processing options
When attachments are enabled, `attachments` includes conventional attachments,
inline MIME parts (such as images referenced by `cid:` URLs in `html_body`), and
filename-bearing MIME parts that omit the `Content-Disposition` header.
**Use cases**:
- Support ticket systems
- Email-to-ticket conversion
- Automated email processing
- Reply handling
**When it's sent**: When an inbound email matches a configured route.
tags:
- Route Events
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RouteWebhookPayload'
example:
type: "message.routing"
route_id: "abe11757-2886-4b55-96f1-0e0afc95795a"
timestamp: "2024-05-06T13:15:46.404433272Z"
data:
id: "route-msg-12345"
from: "customer@gmail.com"
reply_to: "customer@gmail.com"
to: "support@yourdomain.com"
subject: "Help with my account"
message_id: "<unique-message-id@gmail.com>"
size: 2048
spam_score: 0.1
bounce: false
cc: ""
date: "Mon, 06 May 2024 13:15:46 +0000"
in_reply_to: ""
references: ""
auto_submitted: ""
html_body: "<p>I need help with my account settings.</p>"
plain_body: "I need help with my account settings."
reply_from_plain_body: "I need help with my account settings."
attachments:
- filename: "logo.png"
content_type: "image/png"
content_id: "logo-123"
disposition: "inline"
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
headers:
"X-Mailer": "Gmail"
"X-Priority": "3"
responses:
'200':
description: Webhook received successfully
'201':
description: Webhook received successfully
'202':
description: Webhook received successfully
'204':
description: Webhook received successfully
components:
schemas:
# Webhook payload schemas
MessageWebhookPayload:
type: object
required:
- type
- webhook_id
- timestamp
- data
properties:
type:
type: string
enum:
- message.reception
- message.delivered
- message.transient_error
- message.failed
- message.bounced
- message.suppressed
- message.opened
- message.clicked
description: The event type
webhook_id:
type: string
description: The webhook ID (the same ID as returned by the [Create Webhook API](https://ahasend.com/docs/api-reference/webhooks/create-webhook))
format: uuid
timestamp:
type: string
format: date-time
description: When the event occurred (ISO 8601 format)
data:
$ref: '#/components/schemas/MessageWebhookData'
MessageReceptionWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.reception
MessageDeliveredWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.delivered
MessageTransientErrorWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.transient_error
MessageFailedWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.failed
MessageBouncedWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.bounced
MessageSuppressedWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.suppressed
MessageOpenedWebhookPayload:
allOf:
- $ref: '#/components/schemas/MessageWebhookPayload'
- type: object
properties:
type:
enum:
- message.opened
MessageClickedWebhookPayload:
type: object
required:
- type
- webhook_id
- timestamp
- data
properties:
type:
type: string
enum:
- message.clicked
description: The event type
webhook_id:
type: string
description: The webhook ID (the same ID as returned by the [Create Webhook API](https://ahasend.com/docs/api-reference/webhooks/create-webhook))
format: uuid
timestamp:
type: string
format: date-time
description: When the event occurred (ISO 8601 format)
data:
$ref: '#/components/schemas/MessageClickedWebhookData'
MessageWebhookData:
type: object
required:
- account_id
- event
- from
- recipient
- subject
- message_id_header
- id
properties:
account_id:
type: string
format: uuid
description: Account ID that sent the message
event:
type: string
enum:
- on_reception
- on_delivered
- on_transient_error
- on_failed
- on_bounced
- on_suppressed
- on_opened
- on_clicked
description: The specific event that occurred
from:
type: string
format: email
description: Sender email address
recipient:
type: string
format: email
description: Recipient email address
subject:
type: string
description: Email subject line
message_id_header:
type: string
description: Message ID header (same as returned by the [Create Message API](https://ahasend.com/docs/api-reference/messages/create-message))
id:
type: string
description: Internal AhaSend message ID
user_agent:
type: string
description: User agent (only for opened/clicked events)
ip:
type: string
description: IP address (only for opened/clicked events)
is_bot:
type: boolean
description: |
Whether the action was performed by a bot.
Bot detection runs on every open and click and always yields a definite
true or false. Current deliveries include the field on every message
event; it is meaningful for `message.opened` and always `false`
elsewhere. Older deliveries may omit it — absence carries no signal, so
do not infer `false` from a missing field and never reject a delivery
because of it.
delivery_attempt:
description: |
Diagnostics for the delivery attempt this event reports on: the
SMTP status code, the response text, and — on failures — the
bucket the bounce classifier assigned.
Optional, and absent more often than present. Today it is
`message.delivered`, `message.bounced`, and
`message.transient_error` that carry it, and only when an SMTP
attempt was actually recorded. These are the cases where no attempt
is recorded, and so no object is sent:
- **Retry exhaustion.** A message that runs out of retries arrives
as `message.failed`, which reports the exhaustion rather than a
single attempt.
- **Out-of-band bounces**, where the DSN arrives separately after
the destination already accepted the message.
- **Non-SMTP routing**, where the message was handled outside SMTP.
Sandbox sends are the exception: they synthesize a representative
attempt rather than omitting one.
- **Empty responses**, carrying neither a status code nor any text.
These cases are not closed, and an explicit `null` means the same
as a missing field. Always read the object with optional chaining
and never treat its absence as an error. Equally, do not treat its
presence on another event sharing this data shape as impossible.
Events with their own data shape — `message.clicked`,
`message.routing`, `suppression.created`, `domain.dns_error` —
never carry one.
On test webhooks and sandbox deliveries the values are
representative rather than observed. A sandbox simulation can
replace the sample classification with the classification of the
selected outcome, so it may not agree with the representative SMTP
code and response. Do not calibrate a `classification` switch
against sandbox traffic.
$ref: '#/components/schemas/DeliveryAttempt'
MessageClickedWebhookData:
type: object
required:
- account_id
- event
- from
- recipient
- subject
- message_id_header
- url
- user_agent
- ip
- id
properties:
account_id:
type: string
format: uuid
description: Account ID that sent the message
event:
type: string
description: The specific event that occurred
from:
type: string
format: email
# --- truncated at 32 KB (47 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/ahasend/refs/heads/main/asyncapi/ahasend-webhooks-openapi.yaml
Work with this as data
Every AsyncAPI spec here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for asyncapi
4 MCP tools reach this
find_asyncapisBrowse and filter every AsyncAPI spec in the catalog.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.
Call it yourself
curl for this page
curl "https://apis.io/api/v1/asyncapis/ahasend-webhooks-openapi"
curl "https://apis.io/api/v1/asyncapis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.