---
title: Incoming webhooks, bounce webhooks and API keys
description: "The Webhooks & API screen, how to start a flow with a signed incoming webhook, and how to send bounce reports from Mailgun, SendGrid or code."
url: "https://hiveplugins.com/docs/hive-mail-webhooks-and-api-keys/"
updated: "2026-10-28"
category: Hive Mail
---

# Incoming webhooks, bounce webhooks and API keys

Hive Mail has two webhooks, both incoming: an **incoming webhook** that starts a flow, and **bounce webhooks** that report a bounced or complained address. API keys let a program call the REST API. There is no outgoing webhook per event; use a [PHP hook](/docs/hive-mail-hooks-actions/) or the **Call a webhook** flow step ([Steps](/docs/hive-mail-steps/)).

## The Webhooks & API screen

Open **Settings > Webhooks & API** (needs **Manage settings**). Every card acts at once; nothing has a Save button.

![The Webhooks & API screen with its three cards](https://hiveplugins.com/wp-content/uploads/2026/10/hivemail-api-webhooks-and-api-overview-01.webp)

- **The Hive Mail API.** The **API address** (`https://example.com/wp-json/hivemail/v1`) with **Copy address**, and **Open the address**, which shows the route list in the browser. On a site without HTTPS a warning, **This site does not use HTTPS**, sits under it.
- **API keys.** An API key is an application password of the signed-in WordPress user. Type a **Name of the new key**, click **Create key**, copy the password with **Copy password** (shown once), click **I have copied it**. The list shows **Name**, **Created** and **Last used**. **Revoke** stops a key at once. A key has no scopes of its own: it can do what its user can, so limit it with a role ([Roles & Permissions](/docs/hive-mail-settings-roles-and-permissions/)). Routes: `GET`/`POST /api-keys`, `DELETE /api-keys/{uuid}`, `GET /api-keys/info` (all `hivemail_manage_settings`). If the card says **Keys cannot be created here**, WordPress has application passwords switched off: see [authentication](/docs/hive-mail-rest-api-authentication/).
- **Incoming webhook addresses.** A read-only list of every flow whose trigger is **An incoming webhook arrives**, with its state, the address and **Copy address**. It needs **Manage automations** as well (`GET /api-keys/webhooks`).

## Start a flow with an incoming webhook

1. In **Automation > Flows** make a flow. As the trigger choose **An incoming webhook arrives** (group **Webhooks**).
2. In **Address and secret**, copy the **Address (send a POST here)**. To sign requests, show and copy the **Secret (to sign requests)**.
3. Set the options, add the steps, and switch the flow on. The address answers only while the flow is **On**.
4. Make the other service send a `POST` to the address.

The address is not under `/wp-json`, and the secret is never part of it. The number before the dot is the flow id:

```
https://example.com/?hivemail=hook&t=12.0a1b2c3d4e5f60718293a4b5c6d7e8f9
```

| Option on the trigger | What it does |
| --- | --- |
| **Email field** | The key in your request that holds the address. Empty finds it automatically. |
| **Keep these on the contact** | Rows copying a request key to a contact field. First and last name are found by themselves. |
| **Permission to email** | **Subscribe when a consent box is ticked** (needs **Consent box field**; the default, and the flow cannot be switched on without it), **The form itself asks for permission: subscribe everyone who sends it**, or **Do not subscribe anyone: keep them pending**. |
| **Ask pending contacts to confirm by email (double opt-in)** | Sends the confirmation email to people who stay pending. |
| **Add them to this list**, **Give them this tag** | Where the person lands. |
| **When they are already a contact** | **Fill in only the details they have not got yet** or **Update their details with what they just sent**. |
| **Refuse requests that are not signed** | Off by default. |

> **Warning:** Choose "subscribe everyone who sends it" only when the sending service really collected permission. Hive Mail keeps what you tell it as the proof.

### The request

Send a `POST` with a JSON object or a urlencoded form, at most 65,536 bytes, containing an email address.

```
curl -X POST -H "Content-Type: application/json" 
  -d '{"email":"jane@example.com","first_name":"Jane"}' 
  "https://example.com/?hivemail=hook&t=12.0a1b2c3d4e5f60718293a4b5c6d7e8f9"
```

To sign, add `X-HiveMail-Signature: sha256=` followed by the HMAC-SHA256 of the raw body with the flow's secret as the key (the `sha256=` prefix is optional). A signature you send must be right; with **Refuse requests that are not signed** on, a request without one is refused.

```
<?php
$secret = 'the-flow-secret';
$body   = wp_json_encode( array( 'email' => 'jane@example.com', 'first_name' => 'Jane' ) );
wp_remote_post(
    'https://example.com/?hivemail=hook&t=12.0a1b2c3d4e5f60718293a4b5c6d7e8f9',
    array(
        'headers' => array(
            'Content-Type'         => 'application/json',
            'X-HiveMail-Signature' => 'sha256=' . hash_hmac( 'sha256', $body, $secret ),
        ),
        'body'    => $body,
    )
);
```

### Answers

JSON `{ "ok": true|false, "status": "...", "message": "..." }`. `message` is left out of a plain `enrolled`.

| HTTP | `status` | Meaning |
| --- | --- | --- |
| 200 | `enrolled` | The person was created or updated and entered the flow |
| 200 | `not_enrolled` | The re-entry rule says they cannot enter again |
| 200 | `duplicate` | The same body for the same person within the last hour |
| 400 | `bad_body` | Not a JSON object or a form |
| 401 | `bad_signature` | The signature does not match |
| 404 | `not_found` | Wrong or replaced address, or the flow is not a webhook flow |
| 405 | `method_not_allowed` | Not a `POST` |
| 409 | `flow_off` | The flow is not switched on |
| 413 | `too_large` | Over 65,536 bytes |
| 422 | `no_email`, `no_contact` | No usable address, or none could be made (invalid or suppressed) |
| 429 | `rate_limited` | Over 300 requests a minute for this flow; `Retry-After: 60` |

To replace the address and secret, click **New address and secret**, then **Make new ones** in the trigger panel (or `POST /flows/{id}/webhook/rotate`). The old ones stop at once.

> **Tip:** The address is a secret. Share it only with the service that needs it, and turn on signing for anything sensitive.

## Bounce webhooks

A bounce is an email that could not be delivered; a complaint is a spam report. Your sending service tells Hive Mail, which then stops emailing that address ([Bounces and suppressions](/docs/hive-mail-bounces-and-suppressions/)). The addresses are on **Deliverability > Bounces & Suppressions**, card **Bounce webhooks**, and help repeats in **Settings > Sending > Bounces and complaints**.

![The Bounce webhooks card with the generic, Mailgun and SendGrid addresses](https://hiveplugins.com/wp-content/uploads/2026/10/hivemail-api-bounce-webhooks-card-02.webp)

| Service | Address |
| --- | --- |
| Generic (JSON) | `https://example.com/wp-json/hivemail/v1/webhooks/generic` |
| Mailgun | `https://example.com/wp-json/hivemail/v1/webhooks/mailgun` |
| SendGrid | `https://example.com/wp-json/hivemail/v1/webhooks/sendgrid` |

Each request is checked first. An unknown service, or a missing or wrong secret or signature, is `401 hivemail_webhook_unauthorized`. On the card the Mailgun and SendGrid blocks show **Key saved** or **Key needed**.

**Mailgun.** In Mailgun open **Sending > Webhooks** and add the address for permanent failure, temporary failure and spam complaints. Copy Mailgun's HTTP webhook signing key into **Settings > Sending > Mailgun webhook signing key** and save. Requests more than a day old are refused. Or set `HIVEMAIL_MAILGUN_SIGNING_KEY` in `wp-config.php`.

**SendGrid.** In SendGrid open **Settings > Mail Settings > Event Webhook**, enter the address, tick **Bounced**, **Dropped** and **Spam Reports**, turn on **Signature Verification** and copy the key into **Settings > Sending > SendGrid webhook verification key**. Without the key every SendGrid request is refused. Constant: `HIVEMAIL_SENDGRID_WEBHOOK_KEY`.

**Any other service, or your code.** Post JSON to the generic address with the secret in the header `X-HiveMail-Secret` (the card shows it under **Show**; **New secret** replaces it).

```
curl -X POST -H "Content-Type: application/json" 
  -H "X-HiveMail-Secret: THE_SECRET_FROM_THE_CARD" 
  -d '{"type":"bounce","email":"someone@example.com","reason":"mailbox does not exist"}' 
  "https://example.com/wp-json/hivemail/v1/webhooks/generic"
# 200 {"suppressed":1,"noted":0,"ignored":0}
```

`type` is `bounce`, `soft_bounce` or `complaint`; `reason` is optional. You can send an array of such objects, or `{"events":[...]}`. A soft bounce is only noted until it repeats (the limit is on [Settings: Compliance](/docs/hive-mail-settings-compliance/)). Bodies over 1 MB are `413`, invalid JSON `400` (`rest_invalid_json`), and at most 500 events are handled.

A bounce or complaint puts the address on the suppression list, sets a matching contact to **Bounced** or **Complained**, and fires `hivemail_bounce_processed`.

Next: [Hooks: actions](/docs/hive-mail-hooks-actions/), [Flows, recipes, forms and shops](/docs/hive-mail-rest-api-automation-and-commerce/).
