The Big Picture
ChimpLink sits in the middle of everything — four different sources feed events into it, and it decides what to do with each one.
Mailchimp hears about almost everything. The Members App only hears about subscription/access changes — and, as of 19 August 2026, brand-new paying members too (see below).
| Inbound endpoint | Source | Guarded by |
|---|---|---|
| POST /memberful-webhook | Memberful — every subscription & member lifecycle event | HMAC signature (X-Memberful-Webhook-Signature) |
| POST /gbx-member-profile-webhook | Old WordPress site — a member edits their profile there | Shared secret in body |
| POST /apply-form-webhook | Apply Now form submission | Shared secret in body |
| POST /quick-lead | Quick Lead capture form | Shared secret in body |
Memberful → ChimpLink
Every subscription & member event ChimpLink listens for
All of these arrive at the one endpoint, /memberful-webhook, distinguished by an event field in the payload.
| Event | What it means | What ChimpLink does |
|---|---|---|
| member_signup | Someone creates a Memberful account (not necessarily paid yet) | Syncs identity to Mailchimp |
| member_updated | Name/email changed on Memberful | Syncs to Mailchimp; logs the diff. Doesn't touch the Members App feed unless a subscription state is also present in the same payload |
| subscription.created | They've just paid and become a member — the moment of conversion | Tags lead_stage = "Converted", syncs to Mailchimp, and forwards to the Members App with identity attached (new — see below) |
| subscription.updated | Plan or billing details changed | Syncs to Mailchimp, forwards access state to the Members App |
| subscription.renewed | Recurring payment succeeded | Syncs to Mailchimp, forwards access state to the Members App |
| subscription.activated | Subscription turned on (e.g. after a pause) | Syncs to Mailchimp, forwards access state to the Members App |
| subscription.expired | Subscription lapsed without renewing | Syncs to Mailchimp, forwards access state to the Members App |
| subscription.deactivated | Subscription turned off directly | Syncs a "no longer active" stub to Mailchimp, tells the Members App has_access = false |
| subscription.deleted | Subscription removed entirely | Same as deactivated, using the last known cached email if the payload doesn't include one |
| member.deleted | The Memberful account itself is deleted | Syncs a blanked-out stub to Mailchimp, tells the Members App has_access = false, clears ChimpLink's own email cache for that member |
An identity change (name/email) isn't an access change — guessing has_access from a payload that doesn't actually carry a subscription state would risk wrongly telling the Members App someone lost access just because they updated their email. So that forward only ever fires when the incoming payload genuinely includes a subscription's active value.
From the website & forms
The other three inbound events
| Endpoint | What it does |
|---|---|
| /gbx-member-profile-webhook | A profile edit on the old WordPress site — synced straight to Mailchimp. Only relevant until that site is fully retired. |
| /apply-form-webhook | A new Apply Now submission — creates/updates the Applicant record in ChimpLink and syncs it to Mailchimp as a lead. |
| /quick-lead | A lighter-weight lead-capture form (e.g. a newsletter signup) — synced to Mailchimp as a lead, not a full Applicant. |
None of these three reach the Members App directly — only Memberful subscription events do that.
ChimpLink → Members App
The event feed Updated 19 Aug 2026
Every time one of the subscription events above fires, ChimpLink forwards a small, already-decided event to the Members App — the Members App never has to re-derive "is this person active" itself, it just trusts what it's told.
POST /webhooks/chimplink-events
X-ChimpLink-Signature: <HMAC-SHA256 of the body>
X-CL-Idempotency-Key: <timestamp|event|member_id>
{
"event": "subscription.created",
"memberful_member_id": "556689",
"has_access": true,
"timestamp": "2026-08-19T12:00:00Z",
"extra": {
"email": "person@example.com",
"first_name": "Jane",
"last_name": "Doe"
}
}
The extra block only ever appears on subscription.created — every other event sends "extra": {}. That's deliberate: subscription.created is the earliest point ChimpLink reliably knows someone is a genuine, paying member, so it's the one moment worth carrying enough identity to create their Members App profile outright.
Before this change, a Members App profile only ever got created the first time someone actually logged in. That meant a member who paid, then closed their browser before the post-checkout redirect finished (or simply never got around to logging in), would never appear in the Directory at all — invisible to every other member, despite being a fully active, paying subscriber. Attaching identity to subscription.created means their row now gets created the moment they convert, whether or not they ever log in.
Delivery is fire-and-forget with retries: four attempts (0.5s, 1s, 2s, 4s backoff), and a failure here never blocks or breaks the Mailchimp sync running alongside it.
Inside the Members App
What happens on receipt
The Members App verifies the signature, then does one of three things depending on whether it already knows this person and what it was told:
| Outcome | When | What happens |
|---|---|---|
| created | No existing row, has_access is true, and an email was included | A new profile is created immediately (name, email, access), then the same one-time ChimpLink Applicant pull that normally runs at first login is scheduled in the background — so a proactively-seeded member ends up exactly as filled-in as one who logged in first. |
| ok | The row already exists | Just updates has_access and the last-verified timestamp — the same behavior as before this change. |
| no_op | No existing row, and either has_access is false or no email was sent (i.e. any event except subscription.created) | Nothing to create yet — safe to ignore. They'll still be picked up normally the first time they log in. |
Both paths — first login and this webhook — try to create the same row, keyed on Memberful's member ID, which is set to be unique in the database. Whichever one gets there first wins; the other one detects the row already exists and simply updates it instead, so there's no risk of a duplicate or a crash from the ordering being unpredictable.
Members App → ChimpLink
The two direct, machine-to-machine calls
Separately from the event feed above, the Members App calls ChimpLink directly in two places — both authenticated with a shared secret header rather than the HMAC scheme used for webhooks, since these are simple request/response calls rather than fire-and-forget notifications.
| Call | When | Purpose |
|---|---|---|
| GET /applicants/by-member/<id> | Once, at a member's first login (or now, right after their row is proactively created above) | Pulls their existing Applicant record so their new profile isn't blank — company, job title, industry, etc. |
| POST /applicants/profile-update | Any time a member edits their profile in the Members App | Reverse sync — keeps ChimpLink's Applicant record and Mailchimp from going stale after a live edit. |
See How the New Members App Works for the first-login sequence in full, with diagrams.