Skip to main content

Tag Manual Grants with User Role

Premium Beginner

The two manual-grant lifecycle actions form a symmetric pair — one fires when a subscriber is comped via the admin “Add subscriber manually” flow, the other when the hourly expiry cron auto-revokes an expired grant. This recipe hooks both to assign a WordPress role at grant time and remove it on expiry, keeping external systems (LMS enrollment, Discord bots, CRM segments) coherent across the full lifecycle.

Notes: (1) benecaster_manual_subscriber_created fires only for admin-created grants — not for regular paid signups, bulk enrollment, or bridge-provisioned subscribers. Hook benecaster_subscription_activated for those. (2) benecaster_manual_grant_expired only fires for grants that had a non-null manual_expiry_at; comps with no expiry never trigger it. (3) Admins can also manually revoke a grant via the Subscribers list before expiry — that path fires benecaster_token_revoked, not benecaster_manual_grant_expired; hook that action too for blanket cleanup.

benecaster_manual_subscriber_created has a fifth argument, ?string $joined_at — the operator’s Supporter since date, or null when they left the field blank. The example below deliberately keeps 10, 4 and still works, which is the point: the argument was appended, so a listener written without it is unaffected.

A CRM sync is exactly the consumer that wants it. Push it alongside $expiry_at and the segment carries the supporter’s real tenure rather than the date they were imported.

The formats differ; the clocks do not. $joined_at is usually a bare Y-m-d and $expiry_at is a full MySQL datetime, but both are UTC. Do not convert either by the site’s gmt_offset — doing so shifts every join date by the offset. Widen the bare date to midnight UTC if you need a datetime. They can share a timezone, but not a formatter.

Code

<?php
// On grant: add the comped subscriber to a "beta-testers" WP role AND
// push them to a CRM segment. Runs on the same request as the admin's
// POST /shows/{id}/subscribers/manual, so it's safe to make outbound
// HTTP calls here — the admin sees the modal spin briefly.
add_action( 'benecaster_manual_subscriber_created', function ( int $user_id, int $show_id, string $tier_slug, ?string $expiry_at ): void {
    $user = get_userdata( $user_id );
    if ( ! $user ) {
        return;
    }

    // Add a WordPress role for gate control on other themes / plugins.
    $user->add_role( 'beta_tester' );

    // Push to a CRM segment via HTTP. $expiry_at is a UTC MySQL
    // datetime string ('YYYY-MM-DD HH:MM:SS') or null for no-expiry.
    wp_remote_post( 'https://crm.example.com/api/tag', [
        'body' => wp_json_encode( [
            'email'      => $user->user_email,
            'tag'        => 'benecaster-comp',
            'show_id'    => $show_id,
            'tier'       => $tier_slug,
            'expires_at' => $expiry_at,
        ] ),
    ] );
}, 10, 4 );

// On auto-expiry: remove the same state. Runs from cron, so an outbound
// HTTP failure will not block anything on the admin side.
add_action( 'benecaster_manual_grant_expired', function ( int $user_id, int $show_id ): void {
    $user = get_userdata( $user_id );
    if ( ! $user ) {
        return;
    }

    $user->remove_role( 'beta_tester' );

    wp_remote_post( 'https://crm.example.com/api/untag', [
        'body' => wp_json_encode( [
            'email'   => $user->user_email,
            'tag'     => 'benecaster-comp',
            'show_id' => $show_id,
        ] ),
    ] );
}, 10, 2 );

View on GitHub →

Hooks Used

Need this built rather than just documented? See our services →