I need help with my account settings.
" 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 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)) url: type: string description: The URL that was clicked user_agent: type: string description: User agent of the client that clicked the link ip: type: string description: IP address of the client that clicked the link id: type: string description: Internal AhaSend message ID is_bot: type: boolean description: | Whether the click was performed by a bot. Bot detection runs on every click, so the field is always present with a definite true or false. It stays optional in this schema for tolerance: if it were ever absent, do not infer `false` and do not reject the delivery. DeliveryAttempt: # `null` is admitted alongside the object deliberately: it carries exactly # the same meaning as the field being absent, so a consumer that rejected # it would answer 400 to a delivery that told it nothing was recorded, and # 100 consecutive failures disable a webhook. Absent, `null`, and an # object are the three states a reader must handle. type: ["object", "null"] description: | Diagnostics from a single delivery attempt: the SMTP status code, the response text, and — on failures — the bucket the bounce classifier assigned. required: - smtp_code properties: classification: type: string description: | Bucket the bounce classifier assigned to this attempt. Absent on successful deliveries, and absent on a failure the classifier did not label. Known values: `InvalidRecipient`, `BadDomain`, `InactiveMailbox`, `InvalidSender`, `QuotaIssues`, `NoAnswerFromHost`, `BadConnection`, `DNSFailure`, `RoutingErrors`, `TransientFailure`, `MessageExpired`, `ProtocolErrors`, `AuthenticationFailed`, `PolicyRelated`, `Uncategorized`. This is **not** a closed set. New buckets can be introduced at any time, and this list describes what is emitted today rather than everything that may ever appear. Branch on the values you know and keep a fallback branch for everything else; never reject a delivery because the value is unfamiliar. Note that `TransientFailure` here is a classification, unrelated to the `message.transient_error` event type or to the `event` field. x-known-values: - InvalidRecipient - BadDomain - InactiveMailbox - InvalidSender - QuotaIssues - NoAnswerFromHost - BadConnection - DNSFailure - RoutingErrors - TransientFailure - MessageExpired - ProtocolErrors - AuthenticationFailed - PolicyRelated - Uncategorized smtp_code: type: integer minimum: 0 description: | SMTP status code recorded for the attempt. Usually this is the code returned by the destination. Always present when this object is present. `0` means AhaSend recorded response content without an SMTP code — an internal error, typically — so do not read `0` as the field being absent. enhanced_status_code: type: string description: | RFC 3463 enhanced status code in `class.subject.detail` form (for example `5.1.1`), when one was recorded for the attempt. response: type: string description: | Text describing the outcome, as the MTA recorded it. Present when non-empty. Two things it is not. It is not reliably code-free: the reply code usually lives in `smtp_code` alone, but some destinations leave the reply code, the enhanced code, or both at the front of the text, so do not assume a bare sentence and do not parse codes back out of it. And it is not always the destination's own words: when the failure was raised inside AhaSend rather than by the destination, this carries AhaSend's description of that failure instead. Read it as human-readable diagnostics. Branch on `classification`, `smtp_code`, and `enhanced_status_code`. description: type: string description: | Human-readable translation of a complex `response`, present only when that translation differs from `response` itself. This is display prose, not an identifier: its wording changes as the translations improve. Never compare it to a literal and never parse it — branch on `classification`, `smtp_code`, or `enhanced_status_code` instead. command: type: string description: | Normalized SMTP command name that was in flight when the attempt was recorded (for example `RCPT TO` or `DATA`), when the MTA reported one. Command arguments such as envelope addresses are not included. SuppressionWebhookPayload: type: object required: - type - webhook_id - timestamp - data properties: type: type: string enum: [suppression.created] 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/SuppressionWebhookData' SuppressionWebhookData: type: object required: - account_id - recipient - created_at - expires_at - reason - sending_domain properties: account_id: type: string format: uuid description: Account ID recipient: type: string format: email description: Suppressed email address created_at: type: string format: date-time description: When the suppression was created expires_at: type: string format: date-time description: When the suppression expires (typically 30 days from creation) reason: type: string description: Reason for suppression (e.g., "Too many hard bounces") sending_domain: type: string description: Domain from which the failed emails were sent DomainWebhookPayload: type: object required: - type - webhook_id - timestamp - data properties: type: type: string enum: [domain.dns_error] 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/DomainWebhookData' DomainWebhookData: type: object required: - domain - account_id - spf_valid - dkim_valid - dmarc_valid - dns_last_checked_at properties: domain: type: string description: Domain name with DNS issues account_id: type: string format: uuid description: Account ID spf_valid: type: boolean description: Whether SPF record is valid dkim_valid: type: boolean description: Whether DKIM record is valid dmarc_valid: type: boolean description: Whether DMARC record is valid dns_last_checked_at: type: string format: date-time description: When DNS was last checked RouteWebhookPayload: type: object required: - type - route_id - timestamp - data properties: type: type: string enum: [message.routing, route.message] x-deprecated-values: [route.message] description: The event type. `message.routing` is canonical; legacy `route.message` remains accepted as deprecated input. timestamp: type: string format: date-time description: When the event occurred (ISO 8601 format) route_id: type: string description: The route ID (the same ID as returned by the [Create Route API](https://ahasend.com/docs/api-reference/routes/create-route)) format: uuid data: $ref: '#/components/schemas/RouteWebhookData' RouteWebhookData: type: object # Mirrors the producer's struct tags: every field except `headers` # carries no `omitempty`, so Go emits it on every delivery. required: - id - from - reply_to - to - subject - message_id - size - spam_score - bounce - cc - date - in_reply_to - references - auto_submitted - html_body - plain_body - reply_from_plain_body - attachments properties: id: type: string description: Unique route message ID from: type: string description: Sender mailbox text reply_to: type: string format: email description: Reply-to email address to: type: string format: email description: Recipient email address (your route endpoint) subject: type: string description: Email subject line message_id: type: string description: Original Message-ID header size: type: integer description: Email size in bytes spam_score: type: number format: float description: Spam score (0.0 to 10.0) bounce: type: boolean description: Whether this is a bounce message cc: type: string description: CC recipients date: type: string description: Date header from the email in_reply_to: type: string description: In-Reply-To header references: type: string description: References header auto_submitted: type: string description: Auto-Submitted header html_body: type: string description: HTML body content plain_body: type: string description: Plain text body content reply_from_plain_body: type: string description: Extracted reply content from plain body attachments: type: array items: $ref: '#/components/schemas/RouteAttachment' description: | MIME attachments, if the route is configured to include them. This includes conventional attachments, inline parts, and filename-bearing parts without a Content-Disposition header. Use `content_id` to resolve `cid:` references in `html_body`. headers: type: object additionalProperties: type: string description: Email headers (if route is configured to include them) RouteAttachment: type: object required: - filename - content_type - content_id - disposition - data properties: filename: type: string description: Attachment filename content_type: type: string description: MIME content type content_id: type: string description: | Content-ID with the angle brackets stripped and the remainder percent-decoded, so `` arrives as `a b@x`. Empty when the part had no Content-ID header — the key is always present. This is what distinguishes an embedded part from a real attachment, not `disposition`: an image referenced by a `cid:` URL commonly arrives with no Content-Disposition header at all. disposition: type: string description: | Content-Disposition type of the MIME part, usually `attachment` or `inline`, and `""` when the part carried no Content-Disposition header. Note that `""` does not mean "not inline". An embedded image sent as a `multipart/related` part with a Content-ID and no Content-Disposition — what Gmail and Outlook produce — arrives with `disposition: ""` and a populated `content_id`. Use `content_id` to separate embedded parts from real attachments. The key is always present, and the value is deliberately unconstrained: it is whatever token the sending mail server wrote, merely lowercased and trimmed of parameters, so tokens other than `attachment` and `inline` (for example `form-data`) do arrive in practice. Do not validate this field against a fixed list of values — an arbitrary sender could then make your receiver fail, and repeated delivery failures can disable the webhook. Treat an unrecognized value as `attachment`. data: type: string description: Base64-encoded attachment data tags: - name: Message Events description: | Webhooks for outbound email delivery tracking. These events track the lifecycle of emails you send through AhaSend, from initial reception through final delivery or failure. - name: Suppression Events description: | Webhooks for email suppression list changes. These events notify you when email addresses are automatically added to your suppression list due to delivery issues. - name: Domain Events description: | Webhooks for domain configuration issues. These events alert you to DNS configuration problems that could prevent email delivery. - name: Route Events description: | Webhooks for inbound email processing. These events deliver inbound emails to your application for processing (e.g., support tickets, replies).