---
title: "Extending Hive Mail: registries and custom code"
description: The registries and extension points a site can use, with three working examples (delivery method, flow trigger, settings tab) and what is internal.
url: "https://hiveplugins.com/docs/hive-mail-extending-hive-mail/"
updated: "2026-10-28"
category: Hive Mail
---

# Extending Hive Mail: registries and custom code

Hive Mail is built from registries. Its own features use them, and so can your code. A registry is read the first time something needs it; at that moment Hive Mail fires the action `hivemail_register_{name}` with the registry object, and you call `$registry->register( $id, $definition )`. Reading the whole list applies the filter `hivemail_{name}` (see [Hooks: actions](/docs/hive-mail-hooks-actions/) and [Hooks: filters](/docs/hive-mail-hooks-filters/)).

Hooks can be added at file load, in a plugin or a must-use plugin. A class that `implements` a Hive Mail interface or `extends` a Hive Mail class must be declared after Hive Mail has loaded: declare it inside a hook callback or in a file you `require` from one. At the top of your file it is a fatal error (`Interface "HiveMailSendingTransportInterface" not found`) whenever your code loads before Hive Mail.

## What you can use

| Extension point | How | Definition |
| --- | --- | --- |
| Delivery methods | Registry `transports`, action `hivemail_register_transports` | `HiveMailSendingTransportInterface`: `label()`, `send( array $email ): array`. Appears in **Settings > Sending > Delivery method** |
| Flow triggers | Registry `triggers`, action `hivemail_register_triggers` | `HiveMailAutomationTrigger`, or data with `TriggersSimpleTrigger`. Fire with `Dispatcher::fire()` |
| Flow steps | Registry `nodes`, action `hivemail_register_nodes` | `HiveMailAutomationNodeType`. The canvas is drawn from its `describe()` |
| Segment rule types | Registry `segment_conditions`, action `hivemail_register_segment_conditions` | `HiveMailAudienceSegmentsSegmentCondition`: `label()`, `group()`, `describe()`, `sanitize()`, `sql()` |
| Plugin integrations | Registry `integrations`, action `hivemail_register_integrations` | `HiveMailAutomationIntegrationsIntegration` (adds triggers, rule types, merge tags when `detect()` is true) |
| Email blocks | Registry `blocks`, action `hivemail_register_blocks` | `HiveMailDesignBlockType` (usually extend `DesignBlock`) |
| Bounce adapters | Registry `bounce_providers`, action `hivemail_register_bounce_providers` | `HiveMailComplianceBounceProvider`. The id is the last part of `/webhooks/{provider}` |
| Delivery channels | Registry `channels`, action `hivemail_register_channels` | `HiveMailQueueChannelInterface` |
| Background jobs | Filter `hivemail_job_handlers` | Job type => class implementing `HiveMailJobsJobHandler` (`type()`, `step()`, `download()`, `cleanup()`) |
| REST routes | Filter `hivemail_rest_controllers` | A class extending `HiveMailApiBaseController` with a no-argument constructor and `register_routes()`; use `$this->route()` and `$this->can( $capability )` |
| Settings | Filter `hivemail_settings_schema` | Add a group or fields; the form is generated |
| Admin menu | Filter `hivemail_admin_nav` | Items `group`, `path`, `label`, `icon`, `desc`, `cap` |
| Admin screens | Action `hivemail_admin_scripts`, then `window.hivemail.registerPage( path, Component, { title } )` or `registerSettingsPanel( key, Component )` | A React component in your own script |
| Recipes, starter designs, import sources | Filters `hivemail_recipes`, `hivemail_template_starters`, `hivemail_migration_sources` | Arrays described in the filters page |
| Capabilities | Filter `hivemail_capabilities` | Name more capabilities |
| Visitor data (read) | `HiveMailIntelligenceReader` | `visitor()`, `visitors_of_contact()`, `sessions()`, `hits()`, `contact_ids_sql()`. Validated arguments, nothing when tracking is off |
| Visitor events | Actions `hivemail_session_started`, `hivemail_hit_recorded`, `hivemail_intel_goal_reached` | See [Hooks: actions](/docs/hive-mail-hooks-actions/) |

The registries `actions`, `conditions` and `abilities` exist in code but nothing reads them. Do not register into them.

### What is supported and what is internal

Supported: the REST routes and their JSON, the action and filter hooks, the registries and interfaces above, `Dispatcher::fire()`, `Reader`, `Settings::get()`, the shortcodes and the `hm_` tables for reading. Internal, and free to change in any release: other classes and methods, the React components and CSS, option names, the exact SQL, stored JSON formats (flow graphs, designs) except through the API, and the AI assistant's code. For writes use the API or a service call such as `ContactService::create()`, never SQL ([Database tables](/docs/hive-mail-database-tables/)).

## Example 1: a delivery method

This sends every email through your own HTTP service. The email array has `to`, `subject`, `html`, `text`, `from_name`, `from_email`, `reply_to` and `headers`. An empty `html` means a plain-text email. Return `sent`, `retry` (Hive Mail tries again later) or `failed`.

```
<?php
// my-gateway.php
add_action( 'hivemail_register_transports', function ( $registry ) {
    require_once __DIR__ . '/class-my-gateway-transport.php';
    $registry->register( 'my_gateway', new My_Gateway_Transport() );
} );
```

```
<?php
// class-my-gateway-transport.php
final class My_Gateway_Transport implements HiveMailSendingTransportInterface {
    public function label(): string {
        return 'My gateway';
    }

    public function send( array $email ): array {
        $response = wp_remote_post( 'https://mail.example.com/send', array(
            'timeout' => 20,
            'headers' => array( 'Authorization' => 'Bearer ' . MY_GATEWAY_KEY ),
            'body'    => wp_json_encode( $email ),
        ) );
        if ( is_wp_error( $response ) ) {
            return array( 'status' => 'retry', 'error' => $response->get_error_message() );
        }
        $code = wp_remote_retrieve_response_code( $response );
        if ( $code >= 200 && $code < 300 ) {
            return array( 'status' => 'sent' );
        }
        return array( 'status' => $code >= 500 || 429 === $code ? 'retry' : 'failed', 'error' => "HTTP $code" );
    }
}
```

Then choose **My gateway** in **Settings > Sending > Delivery method** (it is listed with WordPress mail, Mailgun and SendGrid). Never put a key in an error string; Hive Mail logs and shows it.

## Example 2: a flow trigger

This adds the trigger **A coupon is used**, under the group "Coupons" (the group name is capitalised), to the flow editor and starts the flows that use it. `SimpleTrigger` takes the key, label, group, settings fields, hooks it listens to (none here: we fire it ourselves) and which settings must agree with the event's context (an empty setting means any).

```
<?php
add_action( 'hivemail_register_triggers', function ( $registry ) {
    $registry->register( 'coupon_used', new HiveMailAutomationTriggersSimpleTrigger(
        'coupon_used', 'A coupon is used', 'coupons',
        array( array( 'key' => 'code', 'kind' => 'text', 'label' => 'Coupon code (empty = any)' ) ),
        array(),
        array( 'code' => 'code' )
    ) );
} );

// Wherever your code knows a coupon was used by an email address:
function my_site_coupon_used( string $email, string $code, int $order_id ): void {
    $contact = HiveMailAudienceContactsContactRepository::find_by_email( $email );
    if ( $contact ) {
        // Returns how many flows the contact was enrolled in.
        HiveMailAutomationDispatcher::fire( 'coupon_used', (int) $contact['id'], array( 'code' => $code ), 'order:' . $order_id );
    }
}
```

The last argument is the thing the event is about; a flow set to re-enter "once per thing" enters once per order. `fire()` does nothing for a contact id below 1, and a person who is not a contact gets no flow. Here a flow set to code `SPRING` answers `1` for `SPRING`, and `0` for `OTHER` or a repeat of the same order. The context (`code`) is available to the flow's steps.

## Example 3: a settings tab

This adds a **Support** tab under Settings with one checkbox and one text field. The form is generated from the schema, and values are validated by type.

```
<?php
add_filter( 'hivemail_settings_schema', function ( array $schema ): array {
    $schema['support'] = array(
        'label'       => 'Support',
        'description' => 'Options for my support tools.',
        'fields'      => array(
            'notify'  => array( 'type' => 'bool', 'default' => true, 'label' => 'Tell support when someone unsubscribes' ),
            'address' => array( 'type' => 'email', 'default' => '', 'label' => 'Support address', 'show_if' => array( 'notify' => true ) ),
        ),
    );
    return $schema;
} );

add_filter( 'hivemail_admin_nav', function ( $items ) {
    $items[] = array(
        'group' => 'settings',
        'path'  => '/settings/support',
        'label' => 'Support',
        'icon'  => 'settings',
        'desc'  => 'Options for my support tools',
        'cap'   => 'hivemail_manage_settings',
    );
    return $items;
} );

// Read it anywhere:
$address = HiveMailCoreSettings::get( 'support', 'address', '' );
```

The tab appears in the Settings menu as **Support** and the generated form saves with **Save changes**. Field types are `string`, `email`, `text`, `bool`, `int`, `enum` (with `options`), `color`, `secret`, `multi` and `lines`. A `secret` is never sent back to the browser. Save from code or from the API with `PUT /settings/support` ([REST API](/docs/hive-mail-rest-api-campaigns-segments-reports/)); each save fires `hivemail_settings_updated`.

> **Warning:** Hooks, interfaces and registries are the contract. If you call an internal class and it changes, your code breaks. When in doubt use the REST API from PHP with `rest_do_request()`.

Next: [Hooks: actions](/docs/hive-mail-hooks-actions/), [Hooks: filters](/docs/hive-mail-hooks-filters/), [Integrations overview](/docs/hive-mail-integrations-overview/).
