Requires: Charitable Pro 1.8.16+
Charitable Ambassadors 3.0.0+
When Sarah shares her invite link and Marcus signs up to fundraise, you want Sarah to get credit. Attribution is what makes that happen automatically – without Sarah having to send anyone a code, without Marcus having to remember to mention her, and without you having to keep a spreadsheet of who recruited whom.
This page explains how Charitable Ambassadors makes that connection – first in plain English, then with the full technical details at the bottom for developers.
The Short Version
When someone clicks an invite link, a tiny piece of data (a “cookie”) gets stored in their browser. It tells your site, in effect: “this person came from Sarah.”
The cookie sits there quietly for up to 30 days. If, during that window, they go on to submit a fundraiser – whether they do it right after clicking the link, or three weeks later from a different page on your site – the system reads the cookie, sees they came from Sarah, and credits Sarah with the recruit.
That’s the whole idea. The rest of this page is just the details.
The Journey, Step by Step
1. Sarah shares her link > 2. Marcus clicks > 3. Cookie stored
↓
6. You approve, Sarah gets credit ← 5. Marcus submits ← 4. Marcus browses your site
1. Sarah shares her link
From her My Campaigns page, Sarah copies her personal invite URL and shares it however she wants – email, text, social, in person. Her URL looks something like:
https://yoursite.com/?charitable-invite=jA4HZIx2AhBnouMN
The ?charitable-invite=… part at the end is a unique token that identifies her specifically (and, if she used a per-campaign Recruit button, the specific cause she’s recruiting for).
2. Marcus clicks
Marcus’s browser fetches that URL. Before WordPress renders anything, Ambassadors’ URL handler intercepts the request, looks up the token, and confirms it belongs to a real, active inviter (Sarah).
3. The cookie is stored
The system drops a small cookie in Marcus’s browser:
| Property | Value |
|---|---|
| Name | charitable_invite_token |
| What’s in it | Sarah’s token (the same string from the URL) |
| How long it lasts | 30 days |
| Where it’s visible | Only on your site, not other sites |
That cookie is the breadcrumb. It’s how your site will remember that Marcus came from Sarah, even if he closes the tab and comes back later from a completely different page.
After dropping the cookie, the system redirects Marcus to your invite landing page – the page you configured under Charitable > Ambassadors > Invitations > Landing Page. The URL bar updates so the ?charitable-invite=… parameter is no longer visible (it doesn’t need to be – the cookie’s there now).
4. Marcus browses your site
This is where attribution gets powerful. Marcus might submit his fundraiser right now, or he might:
- Read your “About us” page first
- Check out the cause campaign in detail
- Bookmark the site and come back tomorrow
- Forget for a week, then suddenly remember and search for your site again
Any of those still work, as long as it happens within 30 days and on the same browser. The cookie waits.
5. Marcus submits his fundraiser
Eventually Marcus clicks “Start a fundraiser” (or whatever button leads to your submission form), fills it out, and submits. At the moment WordPress saves his new fundraiser as a draft, Ambassadors’ attribution handler runs:
- It reads the
charitable_invite_tokencookie from Marcus’s browser. - It looks up the token to find the inviter (Sarah).
- It stamps the new fundraiser with two pieces of metadata: “invited by user ID = Sarah” and “via token = T123.”
- It bumps Sarah’s token’s
claim_countby 1.
That stamp is what powers every “recruit” feature in Charitable Ambassadors.
6. You approve, Sarah gets credit
When you approve Marcus’s fundraiser (transitioning it to published – or if you’re on auto-approval, it publishes immediately), three things happen because of that stamp:
- Marcus’s fundraiser shows up in Sarah’s “Your Recruits” view on her My Campaigns page.
- Marcus’s recruit counts toward the Recruitment widget on your Overview dashboard.
- Sarah gets a congratulatory email (“Your recruit’s fundraiser was approved!”) – if you’ve enabled the Email Inviter On Approval setting.
Last-Click vs First-Click – What Happens When Someone Clicks Multiple Links?
Imagine Marcus has been to your site twice:
- Day 1: clicks Sarah’s invite link, reads your site, doesn’t sign up.
- Day 15: sees James’s invite link in a Facebook post, clicks it, signs up that same day.
Who gets credit – Sarah or James?
The default answer is last-click: James gets credit, because his link is the one that converted Marcus from “browsing” to “fundraising.” This is the standard model in referral systems and the most common choice for peer-to-peer programs.
But some organizations prefer first-click attribution: Sarah gets credit because she’s the one who introduced Marcus to your cause. Even though James’s nudge was the closer, Sarah did the harder work of bringing someone unfamiliar into your orbit.
Switching to first-click is a single line of code (see the Developer reference below). The tradeoff is just philosophical – there’s no right answer.
Edge Cases Worth Knowing About
A few situations come up regularly. Here’s what the system does in each:
| Situation | What happens |
|---|---|
| Sarah clicks her own link and tries to sign up | Self-recruit guard. The landing page shows a special “you can’t recruit yourself” variant. Even if Sarah somehow gets to the submission form, the attribution step skips her and logs the attempt. |
| Sarah’s account is deleted between the click and the approval | Marcus’s fundraiser still has the recruit-stamp meta, so the Overview widget still counts him as a recruit – but the inviter shows as “(deleted user)” and the congratulatory email isn’t sent. |
| Sarah revokes her token while Marcus is mid-signup | Attribution is silently skipped. Marcus’s fundraiser is created normally; it just lands without a recruiter credit. |
| Marcus’s 30-day cookie expires before he submits | The submission is unattributed. If Marcus clicks Sarah’s link again before submitting, the cookie is renewed and attribution works. |
| Marcus clicks the link on his phone but signs up on his laptop | The cookie is per-device. Without browser sync (e.g. Chrome sync), the laptop submission lands unattributed. |
| Two clicks of the same link, same browser | The token’s view_count goes up by 1; the cookie’s expiry resets to 30 days from the latest click. Nothing else changes. |
| A caching plugin is serving the landing page from cache | The system emits no-cache headers on the redirect step, and tells Pro’s cache framework to skip the landing page. If your cache plugin still caches it, the cookie may not get set – you’ll see a self-check notice on the Invitations admin tab. |
Where Attribution Shows Up in Your Admin
Once a recruit has been attributed, you’ll see them in these places:
- Overview > Recruitment widget – counts toward Total / Approved / Pending / Rejected, and to the Recruitment time-series chart.
- Overview > Top Recruiters widget – your leaderboard of who’s bringing in the most recruits.
- My Campaigns > Your Recruits (front-end, for the inviter) – Sarah sees Marcus in her list.
- Invitations > Top Recruiters CSV export – exports the full leaderboard for the active date range.
- Invitations > Recent Activity CSV export – chronological log of every recruit event.
Where to Look When Something Seems Wrong
If a recruit doesn’t show up where you expect, check Charitable Tools > Log. Every attribution event writes an entry there:
| Log code | What it means |
|---|---|
invite_clicked | A valid invite URL was clicked. Confirms the click reached your site. |
invite_claimed | A submission was successfully attributed to an inviter. |
self_recruit_skipped | An inviter tried to recruit themselves. Attribution skipped. |
attribution_skipped_revoked_token | The token was revoked between click and submit. |
inviter_deleted_at_approval | Approval email skipped because the inviter’s user no longer exists. |
Filter the log by source: ambassadors_invites to see only invite-related entries.
Developer Reference
The rest of this page is for developers customizing the attribution system.
The cookie
Name: charitable_invite_token
Value: The 16-character base62 token string
Lifetime: 30 days (filterable via charitable_ambassadors_invite_cookie_lifetime)
Path: /
SameSite: Lax
Secure: true when is_ssl(), otherwise false
HttpOnly: false (intentional - may be read by frontend analytics)
The cookie is set with WP’s setcookie() directly, not via JS, so it’s available on the very next request.
The URL handler
Charitable_Ambassadors_Invites::handle_invite_url() is hooked into init at priority 1. It:
- Returns early if
$_GET['charitable-invite']is empty. - Returns early if
is_admin()(admin requests don’t trigger attribution). - Looks up the token via
Charitable_Ambassadors_Invites_Tokens::lookup_by_token(). - Returns early if the token is missing, revoked, or belongs to a deleted user.
- Sets the cookie via
setcookie(). - Calls
Charitable_Ambassadors_Invites_Tokens::increment_view( $token_id ). - Emits
charitable_nocache_headers()(Pro 1.8.15.2+) ornocache_headers()(WP core fallback). - Resolves the landing page via
charitable_ambassadors_get_invites_setting( 'landing_page_id' )and builds a redirect URL withcharitable-invitestripped. wp_safe_redirect( $landing_url, 302 )+exit.
The 302 status code is deliberate so that cache layers don’t memoize the redirect itself – only the destination page, which is dynamic by charitable_is_dynamic_page.
The attribution handler
Charitable_Ambassadors_Invites::on_campaign_submission_save() is hooked into Pro’s charitable_campaign_submission_save action. Signature:
do_action( 'charitable_campaign_submission_save', $data, $campaign_id, $user_id, $form );
The handler is signature-adaptive because the verification harness invokes it with a legacy 2-arg ( $fundraiser_id, $user_id ) shape; in production it always receives the 4-arg shape. The handler:
- Resolves the cookie value (
$_COOKIE['charitable_invite_token']). - Returns early if no cookie.
- Looks up the token; returns early on miss or revoked status.
- Self-recruit guard: returns early if
$token_row->inviter_user_id === (int) $user_id, logsself_recruit_skipped. - Writes the two attribution meta keys:
update_post_meta( $campaign_id, '_charitable_ambassadors_invited_by_user_id', (int) $token_row->inviter_user_id ); update_post_meta( $campaign_id, '_charitable_ambassadors_invited_via_token_id', (int) $token_row->token_id ); - Calls
Charitable_Ambassadors_Invites_Tokens::increment_claim( $token_id ). - Fires
do_action( 'charitable_ambassadors_invite_claimed', $token_row, $campaign_id, $inviter_user_id ). - Logs
invite_claimedto Charitable Tools > Log.
The two post-meta keys
These are the source of truth for everything downstream:
| Meta key | Type | Used by |
|---|---|---|
_charitable_ambassadors_invited_by_user_id | int (WP user ID) | Recruitment widget, Your Recruits view, inviter-on-approval email gate. |
_charitable_ambassadors_invited_via_token_id | int (token_id PK) | Token-level analytics. Lets you trace a recruit back to a specific scoped URL. |
These are never removed by the plugin – even if the inviter is deleted, the meta sticks (you’ll see “(deleted user)” in Top Recruiters). To clear attribution for a specific recruit, delete the post_meta entries directly:
delete_post_meta( $campaign_id, '_charitable_ambassadors_invited_by_user_id' );
delete_post_meta( $campaign_id, '_charitable_ambassadors_invited_via_token_id' );
Switching attribution mode
Add to your theme’s functions.php or a site-specific plugin:
add_filter( 'charitable_ambassadors_invite_attribution_mode', function () {
return 'first_click'; // default is 'last_click'
} );
Under first_click, the URL handler still updates the cookie on every click (so view counts are accurate per inviter), but only sets the cookie value if no existing cookie is present. Once a cookie is set, subsequent clicks update its expiry but not its value.
Under last_click (default), every click sets a new cookie value, replacing any prior inviter.
Filters
| Filter | Default | Purpose |
|---|---|---|
charitable_ambassadors_invite_attribution_mode | 'last_click' | Switch to 'first_click'. |
charitable_ambassadors_invite_cookie_lifetime | 30 * DAY_IN_SECONDS | Cookie lifetime in seconds. |
charitable_ambassadors_invite_cookie_samesite | 'Lax' | SameSite cookie attribute. Use 'Strict' if your invite URLs are only ever clicked from links on your own domain. |
charitable_ambassadors_invite_self_recruit_allowed | false | Set true to disable the self-recruit guard. Not recommended. |
Actions
| Action | Args | Fires when |
|---|---|---|
charitable_ambassadors_invite_url_resolved | $token_row, $request | After URL handler validates the token, before the cookie is set. Use to short-circuit (e.g. ban specific tokens). |
charitable_ambassadors_invite_clicked | $token_row, $request | After the cookie has been set. |
charitable_ambassadors_invite_claimed | $token_row, $fundraiser_id, $inviter_user_id | Successful attribution. |
charitable_ambassadors_invite_self_recruit_skipped | $token_row, $fundraiser_id | Self-recruit guard blocked attribution. |
charitable_ambassadors_invite_attribution_skipped | $token_row, $reason, $fundraiser_id | Catch-all for any non-success attribution outcome. $reason is one of 'revoked_token', 'self_recruit', 'no_cookie', 'deleted_inviter'. |
Logging
Every attribution event is logged to Charitable Tools > Log via charitable_log():
charitable_log( $code, $context, [
'type' => 'addon',
'source' => 'ambassadors_invites',
'level' => 'info', // or 'warning' for skip cases
'user_id' => get_current_user_id(),
] );
Log codes (the $code argument):
| Code | Level | When |
|---|---|---|
invite_clicked | info | URL handler resolved a valid token. |
invite_claimed | info | Submission successfully attributed. |
self_recruit_skipped | warning | Self-recruit guard fired. |
attribution_skipped_revoked_token | warning | Token was revoked between click and submit. |
inviter_deleted_at_approval | warning | Approval email skipped because inviter’s user is gone. |
unconfigured_landing_page_admin_view | info | Admin viewed the Invitations tab while no landing page was configured. |
Caching
The landing page is dynamic (per-user, per-token). The URL handler emits no-cache headers on every invite click, and Pro 1.8.15.2’s charitable_is_dynamic_page filter is set so cache plugins skip the landing page entirely:
add_filter( 'charitable_is_dynamic_page', function ( $is_dynamic, $post_id ) {
if ( charitable_ambassadors_is_invite_landing_page( $post_id ) ) {
return true;
}
return $is_dynamic;
}, 10, 2 );
If you’re on a Pro version older than 1.8.15.2 (no charitable_is_dynamic_page filter), the fallback nocache_headers() call covers the redirect step but won’t help if a caching plugin caches the landing page directly. The Invitations self-check will warn you about this state.
The tokens table
See How invitation data is stored for the full schema. The columns most relevant to attribution:
| Column | Type | Purpose |
|---|---|---|
token_id | BIGINT PK | The integer stamped on the recruit’s fundraiser as _charitable_ambassadors_invited_via_token_id. |
token | VARCHAR(32) | The string that appears in the URL and the cookie. |
inviter_user_id | BIGINT | The WP user ID of the inviter. |
campaign_id | BIGINT NULL | When NOT NULL, the recruit’s submission auto-attaches to this parent campaign. |
status | VARCHAR(20) | 'active' or 'revoked'. Revoked tokens skip attribution. |
claim_count | INT | Bumped by 1 every successful attribution. |
Related
- Invitations – the parent feature doc.
- How invitation data is stored – the custom table lifecycle.
- Hooks & filters in Ambassadors – the full filter and action reference.
Helpful Links
🤝 Get help when you need it
Connect with Customer Support →
📑 Find the guide you need
Browse the Documentation Hub →
⬇️ Download proven strategies, campaign ideas, and expert tools
Get the Fundraising Kit →
💸 Get Free Fundraising Resources
Head to the Charitable Fundraising Hub →
🤔 Got questions about Charitable?
Charitable FAQs →
Need help understanding non-profit terms and jargon?
See our Non-Profit Glossary →

