<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Integrating applications: the send API, mail in to a webhook, and outbound webhooks]]></title><description><![CDATA[<p dir="auto"><em>Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential when the browser making it reports it as cross-site (<code>Sec-Fetch-Site</code>, or an <code>Origin</code> that is not this server's), so a page on another site cannot post to these routes with a credential, whether a key or the administrator password the browser holds; a program or script that sends no such header, as the <code>curl</code> examples here do not, is unaffected. See <a href="/topic/28277">the 6.3.7 announcement</a>. hMailServer 6.3.8 (2 November 2026) answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, so correct a program that sends such a body to these routes; and it raises <code>account.locked</code> only for a locked name that is an account's own. See <a href="/topic/28278">the 6.3.8 announcement</a>.</em></p>
<p dir="auto">Since 6.3.4 hMailServer has three ways for another program to work with the server's mail: a send API, through which an application sends as one sender and asks afterwards what became of each recipient; mail in to a webhook, where the mail for an address or a whole domain is posted to a URL; and outbound webhooks, which tell a program what the server's mail did. Nothing is on until a key, a route or a subscription is made. <strong>The one thing to know first:</strong> each of the three takes a different credential, and the difference is deliberate.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Feature</th>
<th>Routes</th>
<th>Who may use it</th>
</tr>
</thead>
<tbody>
<tr>
<td>The send API</td>
<td><code>/api/v1/send</code></td>
<td>An API key that names a sender and whose role holds <code>send_through_api</code>. Not the administrator password, not an account.</td>
</tr>
<tr>
<td>Mail in to a webhook</td>
<td><code>/api/v1/inbound-webhooks</code></td>
<td><code>view_configuration</code> to read, <code>manage_inbound_webhooks</code> to change. Meant for an API key's role, confined to the application's domain.</td>
</tr>
<tr>
<td>Outbound webhooks</td>
<td><code>/api/v1/webhooks</code></td>
<td>The administrator password, or an account whose role holds the permission. No API key reaches these routes; the refusal is <code>401</code>.</td>
</tr>
</tbody>
</table>
<p dir="auto">All three are made and managed over the REST listener, so <code>RestApiPort</code> has to be on; outbound webhook subscriptions can also be managed over COM, on Windows.</p>
<h3><a class="anchor-offset" name="the-send-api-the-key"></a>The send API: the key</h3>
<p dir="auto"><code>POST /api/v1/me/messages</code> is the webmail's send and stays the webmail's. The send API is for programs.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Route</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>POST /api/v1/send</code></td>
<td>Sends a message as the key's sender. <code>202 {queue_id, message_id, recipients, recorded}</code></td>
</tr>
<tr>
<td><code>GET /api/v1/send</code></td>
<td>The key's own sends, newest first, paged</td>
</tr>
<tr>
<td><code>GET /api/v1/send/{queue_id}</code></td>
<td>One of them, with the state of each address its body named</td>
</tr>
</tbody>
</table>
<p dir="auto">A key sends when two things are true of it: its role holds <code>send_through_api</code>, and it names a sender. Either alone sends nothing. Three fields are set when the key is made, on <code>POST /api/v1/apikeys</code> with the administrator password or on the Control Deck's <strong>API keys</strong> page.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Field</th>
<th>What it is</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>send_from</code></td>
<td>The one sender the key may use: an address of a domain this server hosts, or <code>*@domain</code> for any address of that domain. Empty, the default, means the key cannot send.</td>
</tr>
<tr>
<td><code>send_per_minute</code></td>
<td>The key's own ceiling on messages a minute. <code>0</code>, the default, sets none.</td>
</tr>
<tr>
<td><code>send_per_day</code></td>
<td>The key's own ceiling on messages in any 24 hours. <code>0</code> sets none, which only a key whose sender is an account's address may have.</td>
</tr>
</tbody>
</table>
<p dir="auto">The built-in <code>serveradmin</code> role holds the permission, so a key made with <code>"scope": "full"</code> holds it. No other built-in role does. A custom role is how a key that sends and does nothing else is made:</p>
<pre><code>POST /api/v1/roles
{"name": "mailer", "description": "sends mail", "permissions": "send_through_api"}

POST /api/v1/apikeys
{"label": "shop confirmations", "role": "mailer", "send_from": "orders@example.com",
 "send_per_day": 5000}
</code></pre>
<p dir="auto">The route refuses with <code>400</code>, among other things, a <code>send_from</code> at a domain this server does not host (a domain alias does not count), at a domain that is switched off, on a read-only key, or outside the domains a key is confined to. <strong>A <code>send_from</code> that is <code>*@domain</code>, or an address that is no account's, needs a <code>send_per_day</code> above 0</strong>, because such a sender has no account's sending quota to hold it. A key's sender cannot be changed afterwards: a new sender is a new key.</p>
<p dir="auto"><strong>What <code>*@domain</code> grants.</strong> To the server, such a key is every mailbox of that domain sending as itself. It can post to a members-only list as any member and send as the domain administrator's mailbox. The display name and <code>reply_to</code> are the application's own and are not checked against anything; what the key vouches for is the address.</p>
<p dir="auto"><strong>On Windows</strong>, the desktop Control Panel's key page does not show these three fields and cannot set them, so a key made there cannot send. The Control Deck and the route do.</p>
<h3><a class="anchor-offset" name="the-send-api-sending-and-asking-afterwards"></a>The send API: sending and asking afterwards</h3>
<pre><code class="language-bash">curl https://mail.example.com:8443/api/v1/send \
  -H "Authorization: Bearer hmapi_0123…" \
  -H "Content-Type: application/json" \
  -d '{"to":["ann@example.net"],"subject":"Your order","text":"Thank you."}'
</code></pre>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Member</th>
<th>Rule</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>from</code></td>
<td>Optional when the key's sender is an address, required when it is <code>*@domain</code>. <code>"address"</code> or <code>"Name &lt;address&gt;"</code>.</td>
</tr>
<tr>
<td><code>to</code>, <code>cc</code>, <code>bcc</code></td>
<td>Arrays of mailboxes; at least one recipient between them. Bcc goes to the envelope and nowhere else.</td>
</tr>
<tr>
<td><code>reply_to</code></td>
<td>Optional.</td>
</tr>
<tr>
<td><code>subject</code></td>
<td>Required; it may be empty.</td>
</tr>
<tr>
<td><code>text</code>, <code>html</code></td>
<td>At least one. Both makes a <code>multipart/alternative</code>.</td>
</tr>
<tr>
<td><code>headers</code></td>
<td>Optional: <code>X-</code> fields other than the server's own <code>X-hMailServer-</code> fields, <code>List-Unsubscribe</code>, <code>List-Unsubscribe-Post</code>, <code>In-Reply-To</code> and <code>References</code>. Any other field is <code>400</code>.</td>
</tr>
<tr>
<td><code>attachments</code></td>
<td>Optional: <code>[{filename, content_type, data}]</code>, <code>data</code> in base64, at most 20 and twelve megabytes together.</td>
</tr>
</tbody>
</table>
<p dir="auto">A line break is refused with <code>400</code> in every value that becomes a header, and a member the route does not take (<code>body</code> for <code>text</code>, for example) is <code>400</code> and named, not dropped.</p>
<p dir="auto"><strong>What the server does to the message.</strong> It is written and checked by the same code as the webmail's send. Every recipient goes through what <code>RCPT TO</code> asks of an authenticated sender, with the server's recipient ceiling (<code>MaxSMTPRecipientsInBatch</code>) and the suppression list. The domain's signature and disclaimer are added, the rules that block are asked (<code>403</code>, <code>policy_rejected</code>), and the message is held to the maximum message size (<code>413</code>). It is DKIM-signed at delivery by the domain's key. The envelope sender is the From address, so a bounce comes back to it. A <code>Received</code> field names the server, the send API and the key's label, never the key's id. <strong>No Sent copy is kept anywhere.</strong> When the sender is an account's address, that account's sending quota is consumed as an SMTP submission would consume it.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Ceiling</th>
<th>Answer</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>send_per_minute</code></td>
<td><code>429</code>, <code>Retry-After: 60</code></td>
</tr>
<tr>
<td><code>send_per_day</code></td>
<td><code>429</code>, <code>Retry-After: 3600</code></td>
</tr>
<tr>
<td>The sender account's sending quota</td>
<td><code>429</code>, <code>Retry-After</code> one twenty-fourth of the account's period</td>
</tr>
<tr>
<td>The REST request budget, 200 requests in 10 seconds</td>
<td><code>429</code>, <code>Retry-After: 10</code></td>
</tr>
<tr>
<td>The recipient ceiling, the size</td>
<td><code>400</code>, <code>413</code></td>
</tr>
</tbody>
</table>
<p dir="auto">A message is counted only once every other check has passed, and when a later ceiling refuses, every earlier one is given back. A refused message is never a counted one.</p>
<p dir="auto">The answer to a message that is taken is <code>202</code>:</p>
<pre><code>{"queue_id": 4711, "message_id": "&lt;…@mail.example.com&gt;", "recipients": 2, "recorded": true}
</code></pre>
<p dir="auto"><code>queue_id</code> is the one to ask about afterwards with <code>GET /api/v1/send/4711</code>. The answer names only the addresses the body named, each with a state.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>State</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>queued</code></td>
<td>In the queue and not yet tried</td>
</tr>
<tr>
<td><code>deferred</code></td>
<td>An attempt failed and will be retried</td>
</tr>
<tr>
<td><code>delivered</code></td>
<td>The trace's <code>delivered</code> event</td>
</tr>
<tr>
<td><code>bounced</code></td>
<td>Refused for good, or the retries ran out</td>
</tr>
<tr>
<td><code>refused</code></td>
<td>The suppression list stopped it at delivery</td>
</tr>
<tr>
<td><code>unknown</code></td>
<td>Nothing names the address; the answer says why</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>It needs the message trace, and the trace is off by default</strong> (<code>MessageTraceEnabled</code>), because it records who corresponds with whom. With it off, a send reads <code>queued</code> or <code>deferred</code> while it is in the queue and <code>unknown</code> once the queue has let it go; <code>trace</code> in the answer is <code>false</code> and a <code>trace_note</code> says so. Turn the trace on, and choose its retention, before relying on the answer. The record of a send is kept for <code>MessageTraceRetentionDays</code>, thirty by default. Only a send this key made is answered; any other id is the same <code>404</code>. The backup does not carry the records.</p>
<h3><a class="anchor-offset" name="mail-in-to-a-webhook-a-route"></a>Mail in to a webhook: a route</h3>
<p dir="auto">SendGrid and Mailgun call this inbound parse. A route is made over REST or on the Control Deck's <strong>Inbound webhooks</strong> view. There is no COM member and no Control Panel page, on either platform.</p>
<pre><code>POST /api/v1/inbound-webhooks
{
  "address": "support@example.com",
  "url": "https://helpdesk.example/hooks/mail",
  "secret": "a-long-random-string"
}
</code></pre>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Field</th>
<th>What it is</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>address</code></td>
<td><code>user@example.com</code>, or <code>example.com</code> for every address of the domain. The domain must be one this server hosts.</td>
<td>required</td>
</tr>
<tr>
<td><code>url</code></td>
<td><code>https://</code> to the endpoint; plain <code>http://</code> only to this machine. Printable ASCII only.</td>
<td>required</td>
</tr>
<tr>
<td><code>secret</code></td>
<td>Write-only. Every request is signed under it and it is never returned.</td>
<td>required</td>
</tr>
<tr>
<td><code>deliver</code></td>
<td><code>false</code>: the route alone receives the mail. <code>true</code>: what the address reaches without the route receives it as well.</td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>on_failure</code></td>
<td><code>keep</code> or <code>bounce</code></td>
<td><code>keep</code></td>
</tr>
<tr>
<td><code>format</code></td>
<td><code>form</code> (<code>multipart/form-data</code> in SendGrid's field names) or <code>json</code></td>
<td><code>form</code></td>
</tr>
<tr>
<td><code>attachments</code></td>
<td><code>include</code>, <code>names</code> or <code>none</code></td>
<td><code>include</code></td>
</tr>
<tr>
<td><code>include_raw</code></td>
<td>Also send the message as it arrived</td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>max_size_kb</code></td>
<td>A larger message is not posted. At most <code>81920</code>.</td>
<td><code>10240</code></td>
</tr>
<tr>
<td><code>enabled</code></td>
<td>A route switched off is as though it were not there for new mail</td>
<td><code>true</code></td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>A route decides that the address exists.</strong> <code>RCPT TO</code> is accepted for a routed address whether or not an account, an alias or a list has it, and a route is asked before any of them. With <code>deliver</code> off, the route takes the mail instead of whatever has the address, so a list whose address a route takes never sees the message. Route a list's domain with care. An address route wins over its domain's, and a recipient with a plus tag (<code>reply+1234@example.com</code>) is found by the address as written, then by the address before the tag, then by the domain, with the tag kept in what is posted.</p>
<p dir="auto"><strong>What arrives</strong> is a signed <code>POST</code>:</p>
<pre><code>POST /hooks/mail HTTP/1.0
Content-Type: multipart/form-data; boundary=hMailServer-4193-...
X-hMailServer-Timestamp: 1789000042
X-hMailServer-Signature: sha256=&lt;hex&gt;
X-hMailServer-Event: message.inbound
X-hMailServer-Delivery: 4193
X-hMailServer-Attempt: 1
</code></pre>
<p dir="auto"><code>X-hMailServer-Delivery</code> is the same on every attempt. The <code>form</code> format carries SendGrid's fields (<code>headers</code>, <code>from</code>, <code>to</code>, <code>cc</code>, <code>subject</code>, <code>text</code>, <code>html</code>, <code>envelope</code>, <code>sender_ip</code>, <code>SPF</code>, <code>dkim</code>, <code>charsets</code>, <code>attachments</code>, <code>attachment-info</code>, <code>attachment1</code> onward) with a few of this server's own beside them (<code>spam</code>, <code>dmarc</code>, <code>route</code>, <code>queue_id</code>, <code>attachments_left_out</code>). Mailgun's field names are not sent. The <code>json</code> format is this server's own shape. The <code>SPF</code>, <code>dkim</code> and <code>dmarc</code> results are the ones this server wrote into its own <code>Authentication-Results</code> field, and only while <code>AuthenticationResultsEnabled</code> is on.</p>
<p dir="auto"><strong>Retries and what is kept.</strong> The message is taken after the global rules, the virus scan and the sandbox have had it, and nothing is posted from the delivery thread. Any <code>2xx</code> delivers. Anything else, a redirect included, is a failure. The retries are the outbound webhooks': <code>WebhookMaxAttempts</code> attempts (5 by default), with waits of 0, 1, 5, 15 and then 60 minutes. When they run out:</p>
<ul>
<li><strong><code>keep</code></strong>: the message is kept and never swept. It can be read with <code>GET /api/v1/inbound-webhooks/deliveries/{id}/message</code>, tried again with <code>POST /api/v1/inbound-webhooks/deliveries/{id}/retry</code> or removed with <code>DELETE /api/v1/inbound-webhooks/deliveries/{id}</code>. The error log says so once, as <code>HM7401</code>.</li>
<li><strong><code>bounce</code></strong>: the message is returned to its sender with a delivery-status report (<code>4.4.7</code>), without repeating what the endpoint answered. <code>HM7402</code> records the return. Where the sender cannot be told, the message is kept instead.</li>
</ul>
<p dir="auto">A message larger than the route's ceiling is not posted at all: it is returned at once (<code>5.3.4</code>) or kept at once (<code>HM7403</code>). Delivered and returned rows are removed after <code>WebhookRetentionDays</code> (7 by default), and the rows survive a restart.</p>
<p dir="auto"><strong>Where an endpoint may be</strong> depends on who made the route. The server's administrator may name an address only this server can reach, but never a link-local or cloud metadata address (<code>169.254.0.0/16</code>, <code>fe80::/10</code> and the clouds' own metadata addresses). Every other credential, an API key of any scope or role included, may name a public address only. Both are judged when the route is saved and again at every connection. No redirect is followed and <code>HttpProxy</code> is never used.</p>
<p dir="auto"><strong>Who may.</strong> Only <code>serveradmin</code> holds <code>manage_inbound_webhooks</code> as shipped, since routing a domain's mail to a URL is as much as reading all of it. A reader without it is answered a route's URL as its scheme and host alone, so put a token in the secret, not in the URL. Reading a kept message needs <code>view_mailbox</code> as well. <code>POST /api/v1/inbound-webhooks/run</code> makes one pass now and needs a credential that reaches every domain.</p>
<h3><a class="anchor-offset" name="outbound-webhooks"></a>Outbound webhooks</h3>
<p dir="auto"><strong>An outbound webhook does not carry the message.</strong> No body, no attachments, no header block. It is a notification. The one piece of content it can carry is the subject, and only where the domain's <code>webhook_subjects</code> and the subscription's <code>include_subject</code> are both on; both are off by default, and even then a subject travels on <code>message.received</code> and <code>message.quarantined</code> only.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Event</th>
<th>Raised when</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>message.received</code></td>
<td>The message has been saved and the server has taken responsibility for it, once per local recipient</td>
</tr>
<tr>
<td><code>message.delivered</code></td>
<td>A recipient, local or remote, has been delivered to</td>
</tr>
<tr>
<td><code>message.bounced</code></td>
<td>A non-delivery report has been written and queued for the sender, once per failed recipient</td>
</tr>
<tr>
<td><code>message.quarantined</code></td>
<td>The held copy and its row exist</td>
</tr>
<tr>
<td><code>account.locked</code></td>
<td>Repeated authentication failures have locked a name</td>
</tr>
</tbody>
</table>
<p dir="auto">A delivery attempt that failed and will be retried is not a bounce, and no event says <code>deferred</code>.</p>
<pre><code>POST /api/v1/webhooks
{
  "name": "ticketing",
  "url": "https://tickets.example/hooks/mail",
  "secret": "a-long-random-string",
  "events": "message.bounced,account.locked",
  "domains": "example.com",
  "max_per_minute": 120
}
</code></pre>
<p dir="auto">An empty <code>events</code>, <code>domains</code> or <code>addresses</code> list means every one. A subscription confined to a send API key's sender (<code>"addresses": "orders@example.com"</code>) hears <code>message.delivered</code> and <code>message.bounced</code> for the recipients outside this server, with the message trace on or off. Plain <code>http</code> off this machine is refused when the subscription is made. The secret is write-only, protected at rest, and an empty <code>secret</code> in a change keeps the one that is set.</p>
<p dir="auto"><strong>Permissions.</strong> Reading needs <code>view_configuration</code>; deleting needs <code>manage_settings</code>; creating, changing, testing and running a pass need <code>manage_server</code>, because whoever can do those can make the server connect to a host of their choosing. A role confined to named domains is refused all eight routes.</p>
<p dir="auto"><strong>The queue.</strong> The delivery pass runs once a minute, attempts at most 200 rows and runs for at most 60 seconds. A delivery succeeds on any <code>2xx</code>; a request that has taken more than ten seconds is a failure. After <code>WebhookMaxAttempts</code> the row is dead-lettered and the error log says so once as <code>HM6971</code>. Past a subscription's ceiling per minute, events are dropped and counted as <code>dropped</code>, not queued.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Setting</th>
<th>REST key</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>WebhooksEnabled</code></td>
<td><code>webhooks_enabled</code></td>
<td>on</td>
</tr>
<tr>
<td><code>WebhookMaxAttempts</code></td>
<td><code>webhook_max_attempts</code></td>
<td>5</td>
</tr>
<tr>
<td><code>WebhookMaxPerMinute</code></td>
<td><code>webhook_max_per_minute</code></td>
<td>60</td>
</tr>
<tr>
<td><code>WebhookRetentionDays</code></td>
<td><code>webhook_retention_days</code></td>
<td>7</td>
</tr>
</tbody>
</table>
<p dir="auto"><code>WebhooksEnabled</code> silences the outbound webhooks and not the inbound routes. From a shell:</p>
<pre><code class="language-bash">hmctl webhook create --set name=ticketing --set url=https://tickets.example/hook \
                     --set-string secret="$SECRET" --set events=message.bounced --set domains=example.com
hmctl webhooks test --event message.bounced --domain example.com
hmctl webhooks run
hmctl webhooks deliveries --state dead-lettered
</code></pre>
<p dir="auto"><code>webhooks test</code> raises a real event through the path a real one takes and says how many subscriptions took it, so <code>0</code> is an answer. In PowerShell the functions are <code>Get-HmWebhook</code>, <code>New-HmWebhook</code>, <code>Set-HmWebhook</code>, <code>Remove-HmWebhook</code>, <code>Get-HmWebhookDelivery</code>, <code>Invoke-HmWebhookDelivery</code> and <code>Test-HmWebhook</code>. Over COM, on Windows, <code>Utilities.GetWebhookSubscriptions(name)</code> and <code>Utilities.SaveWebhookSubscription(json)</code> take and answer the same JSON documents.</p>
<h3><a class="anchor-offset" name="verifying-a-signature"></a>Verifying a signature</h3>
<p dir="auto">One recipe signs all of them: the outbound webhooks, the inbound routes and the webhook of an alert rule. The signature is <strong>HMAC-SHA256 over the timestamp, a full stop, and the raw request body</strong>, under the shared secret, as lower-case hex, in <code>X-hMailServer-Signature</code>. Verify it against the bytes received, before parsing them.</p>
<pre><code class="language-python">import hmac, hashlib, time

def verify(secret, headers, raw_body):
    timestamp = headers["X-hMailServer-Timestamp"]
    signature = headers["X-hMailServer-Signature"]

    # Refuse anything older than five minutes, or the signature can be replayed.
    if abs(time.time() - int(timestamp)) &gt; 300:
        return False

    expected = hmac.new(secret.encode(), (timestamp + ".").encode() + raw_body,
                        hashlib.sha256).hexdigest()

    return hmac.compare_digest("sha256=" + expected, signature)
</code></pre>
<ol>
<li>Compare in constant time.</li>
<li>Check the timestamp: a signature with no freshness check can be replayed.</li>
<li>De-duplicate on <code>X-hMailServer-Delivery</code> together with <code>X-hMailServer-Event</code>. A retry is the same body with a fresh timestamp and so a fresh signature, and an inbound and an outbound delivery can carry the same number.</li>
<li>To rotate a secret, <code>PUT</code> the new one and accept either signature at the receiver while the change is made. There is no overlap window in the server.</li>
</ol>
<p dir="auto">The documents also give the recipe in JavaScript. An alert's webhook carries three of the headers: <code>X-hMailServer-Timestamp</code>, <code>X-hMailServer-Signature</code> and <code>X-hMailServer-Event</code>.</p>
<h3><a class="anchor-offset" name="what-they-do-not-do"></a>What they do not do</h3>
<ul>
<li><strong>The send API</strong> has no templates, no substitutions, no scheduled send and no per-recipient ceiling of the key's own, and gives no state without the trace once a message has left the queue.</li>
<li><strong>An inbound route</strong> makes one post per route per message, not one per recipient. It has no setting of its own and no per-route retry ladder.</li>
<li><strong>An outbound webhook</strong> does not carry the message and has no per-subscription retry policy.</li>
<li><strong>Neither kind of webhook is in the configuration backup.</strong> Export the routes with <code>GET /api/v1/inbound-webhooks</code> and the subscriptions with <code>hmctl webhook list</code>; the secrets are in no read.</li>
<li><strong>One pass at a time is per process, not per database</strong>, for both kinds of webhook.</li>
</ul>
<h3><a class="anchor-offset" name="where-to-read-more"></a>Where to read more</h3>
<ul>
<li>The operator documents: <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SendApi.md" rel="nofollow ugc">SendApi.md</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/InboundWebhooks.md" rel="nofollow ugc">InboundWebhooks.md</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Webhooks.md" rel="nofollow ugc">Webhooks.md</a> and, for the alerts' webhook, <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/AlertsAndAuditTrail.md" rel="nofollow ugc">AlertsAndAuditTrail.md</a>.</li>
<li>The wiki: <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/Mail-In-to-a-Webhook" rel="nofollow ugc">Mail In to a Webhook</a> and <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/The-REST-API" rel="nofollow ugc">The REST API</a>.</li>
<li>The public documentation: <a href="https://www.progressiverobot.com/hmailserver-documentation/#24-the-rest-api" rel="nofollow ugc">chapter 24, The REST API</a>.</li>
<li>On this forum: <a href="/topic/28192">The Control Deck</a>, the <a href="/topic/28218">6.3.4 announcement</a>, <a href="/topic/28264">the REST API guide</a> and <a href="/topic/28265">hmctl and the PowerShell module</a>; turning the message trace on is in <a href="/topic/28225">Troubleshooting by symptom</a>, and the alert rules whose webhook is signed the same way are in <a href="/topic/28273">Alerts, the audit trail, syslog and the journal</a>.</li>
</ul>
]]></description><link>https://www.hmailserver.co.uk/topic/28266/integrating-applications-the-send-api-mail-in-to-a-webhook-and-outbound-webhooks</link><generator>RSS for Node</generator><lastBuildDate>Fri, 06 Nov 2026 09:05:52 GMT</lastBuildDate><atom:link href="https://www.hmailserver.co.uk/topic/28266.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 27 Sep 2026 19:48:13 GMT</pubDate><ttl>60</ttl></channel></rss>