Charitable Documentation

Learn how to make the most of Charitable with clear, step-by-step instructions.

How Recruit Attribution Works in Charitable Ambassadors | Last-Click vs First-Click

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:

PropertyValue
Namecharitable_invite_token
What’s in itSarah’s token (the same string from the URL)
How long it lasts30 days
Where it’s visibleOnly 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:

  1. It reads the charitable_invite_token cookie from Marcus’s browser.
  2. It looks up the token to find the inviter (Sarah).
  3. It stamps the new fundraiser with two pieces of metadata: “invited by user ID = Sarah” and “via token = T123.”
  4. It bumps Sarah’s token’s claim_count by 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:

SituationWhat happens
Sarah clicks her own link and tries to sign upSelf-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 approvalMarcus’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-signupAttribution is silently skipped. Marcus’s fundraiser is created normally; it just lands without a recruiter credit.
Marcus’s 30-day cookie expires before he submitsThe 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 laptopThe cookie is per-device. Without browser sync (e.g. Chrome sync), the laptop submission lands unattributed.
Two clicks of the same link, same browserThe 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 cacheThe 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 codeWhat it means
invite_clickedA valid invite URL was clicked. Confirms the click reached your site.
invite_claimedA submission was successfully attributed to an inviter.
self_recruit_skippedAn inviter tried to recruit themselves. Attribution skipped.
attribution_skipped_revoked_tokenThe token was revoked between click and submit.
inviter_deleted_at_approvalApproval 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:

  1. Returns early if $_GET['charitable-invite'] is empty.
  2. Returns early if is_admin() (admin requests don’t trigger attribution).
  3. Looks up the token via Charitable_Ambassadors_Invites_Tokens::lookup_by_token().
  4. Returns early if the token is missing, revoked, or belongs to a deleted user.
  5. Sets the cookie via setcookie().
  6. Calls Charitable_Ambassadors_Invites_Tokens::increment_view( $token_id ).
  7. Emits charitable_nocache_headers() (Pro 1.8.15.2+) or nocache_headers() (WP core fallback).
  8. Resolves the landing page via charitable_ambassadors_get_invites_setting( 'landing_page_id' ) and builds a redirect URL with charitable-invite stripped.
  9. 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:

  1. Resolves the cookie value ($_COOKIE['charitable_invite_token']).
  2. Returns early if no cookie.
  3. Looks up the token; returns early on miss or revoked status.
  4. Self-recruit guard: returns early if $token_row->inviter_user_id === (int) $user_id, logs self_recruit_skipped.
  5. 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 );
    
  6. Calls Charitable_Ambassadors_Invites_Tokens::increment_claim( $token_id ).
  7. Fires do_action( 'charitable_ambassadors_invite_claimed', $token_row, $campaign_id, $inviter_user_id ).
  8. Logs invite_claimed to Charitable Tools > Log.

The two post-meta keys

These are the source of truth for everything downstream:

Meta keyTypeUsed by
_charitable_ambassadors_invited_by_user_idint (WP user ID)Recruitment widget, Your Recruits view, inviter-on-approval email gate.
_charitable_ambassadors_invited_via_token_idint (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

FilterDefaultPurpose
charitable_ambassadors_invite_attribution_mode'last_click'Switch to 'first_click'.
charitable_ambassadors_invite_cookie_lifetime30 * DAY_IN_SECONDSCookie 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_allowedfalseSet true to disable the self-recruit guard. Not recommended.

Actions

ActionArgsFires when
charitable_ambassadors_invite_url_resolved$token_row, $requestAfter 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, $requestAfter the cookie has been set.
charitable_ambassadors_invite_claimed$token_row, $fundraiser_id, $inviter_user_idSuccessful attribution.
charitable_ambassadors_invite_self_recruit_skipped$token_row, $fundraiser_idSelf-recruit guard blocked attribution.
charitable_ambassadors_invite_attribution_skipped$token_row, $reason, $fundraiser_idCatch-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):

CodeLevelWhen
invite_clickedinfoURL handler resolved a valid token.
invite_claimedinfoSubmission successfully attributed.
self_recruit_skippedwarningSelf-recruit guard fired.
attribution_skipped_revoked_tokenwarningToken was revoked between click and submit.
inviter_deleted_at_approvalwarningApproval email skipped because inviter’s user is gone.
unconfigured_landing_page_admin_viewinfoAdmin 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:

ColumnTypePurpose
token_idBIGINT PKThe integer stamped on the recruit’s fundraiser as _charitable_ambassadors_invited_via_token_id.
tokenVARCHAR(32)The string that appears in the URL and the cookie.
inviter_user_idBIGINTThe WP user ID of the inviter.
campaign_idBIGINT NULLWhen NOT NULL, the recruit’s submission auto-attaches to this parent campaign.
statusVARCHAR(20)'active' or 'revoked'. Revoked tokens skip attribution.
claim_countINTBumped by 1 every successful attribution.

Related

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 →

🔗 Let your AI Assistant do the fundraising work for you.

Connect Charitable to your AI assistant →

💸 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 →

Still have questions? We’re here to help!

Last Modified:

What's New In Charitable

View The Latest Updates
🔔 Subscribe to get our latest updates
📧 Subscribe to Emails

Email Subscription

Join our Newsletter

We won’t spam you. We only send an email when we think it will genuinely help you. Unsubscribe at any time!

Improvement Payments

💸 Introducing Tiered Rates for Fee Relief: Match the Ask to the Gift!

Fee Relief asks your donors one short question at checkout: would you like to cover the processing fee on this gift? Until now that question used a single rate for every donation on a gateway, so a $10 gift and a $500 gift were asked for the same percentage. Fee Relief 1.3.0 lets you set as many donation bands as you like, each carrying its own rate. Why this is important:

  • 🪙 Gentler on small gifts: A fixed transaction cost is a far bigger share of a $10 donation than a $500 one, so bands let you ask for less at the bottom.
  • 🎚️ As many bands as you need: Each band carries its own percentage and its own fixed amount, and the top band runs on with no upper limit.
  • 🛑 An optional ceiling: A Maximum fee field stops any donor being asked for more than a number you choose, however large the gift.
  • 🔌 Set per payment gateway: Stripe can run on four bands while Windcave and Payfast stay on a single rate, because bands belong to a gateway and not to your whole site.
  • 🛡️ Nothing changes until you say so: Your current rates stay exactly as they are until you switch Use Tiered Rates on under Charitable » Settings » Advanced » Fee Relief.

Check out our announcement here.

Donations Improvement

📊 Your Pro Dashboard Can Now Compare, Export, and Score!

Most fundraising dashboards only tell you how much you have raised. Charitable Pro 1.8.19 rebuilds the dashboard and the Reports Overview around the harder questions: whether anything is broken, whether this period beat the last one, and which single change would make the most difference today. Why this is important:

  • 📈 Every figure has a trend: Pick a period and the tiles and the chart move together, with each total shown against the previous period.
  • 🗓️ Charts that stay readable: Short ranges group donations day by day, while longer ranges group by week or month, so a full year reads as a curve instead of a flat line.
  • 🔀 Compare any 2 periods: Not just consecutive ones, so you can ask how this December did against last December. That matters because fundraising is seasonal.
  • 📤 Export the whole summary: Send the full overview out as CSV or PDF, ready for a board pack or a funder report.
  • 🎯 Score your setup for free: Have your fundraising setup scored, so the one change worth making next is the one you can see.

Check out our announcement here.

Feature New

🎟️ Introducing Charitable Events: Sell Tickets Inside Your Campaigns!

Most nonprofits sell event tickets in one tool and collect donations in another, which leaves two sets of reports and a manual match at the end of the year. Charitable Events brings both together, selling tickets through the same donation forms and payment gateways you already run in Charitable. Why this is important:

  • 🎟️ Every ticket is a donation: Ticket sales run through the gateway you already use and count toward the campaign goal, with everything managed under Charitable » Events.
  • 📱 QR check-in from any phone: Each attendee gets their own QR code on the receipt, and volunteers scan at the door through a check-in link that needs no WordPress account.
  • 🧾 Receipts ready for tax time: Give each ticket type a fair market value, and Charitable puts the correct tax-deductible amount on every donor receipt.
  • 📋 Attendee details at checkout: Ask every guest for a meal choice, shirt size, or access needs, with a separate attendee record created for each ticket.
  • 🗓️ Blocks and calendars included: Three editor blocks plus Sugar Calendar and The Events Calendar support put your events where supporters already look.

Check out our announcement here.

ai New

🚀 Unleash AI Power in Charitable!

Your AI assistant can now build, edit, and manage your Charitable fundraising campaigns directly — just by asking. With Charitable Pro adding write support for the WordPress Abilities API, tools like ChatGPT, Claude, and Cursor can handle full administrative actions safely through MCP. Why this is important:

  • 🤖 Campaign creation in plain language: Prompt your assistant with a goal like “Set up a fundraiser for the shelter with a $5,000 goal”, and it builds a real draft campaign complete with suggested donation tiers.
  • 🎨 Instant donation form tweaks: Add custom fields, change button colors, or reorder elements without diving into form settings.
  • 📝 Offline gift logging: Tell your assistant about cash or check gifts, and it will match donor emails and record transactions into your history.
  • 🛡️ Safety and control by default: Write access stays strictly off until turned on in Charitable » Tools » AI MCP, keeping read-only reporting safe by default.
  • 📜 Full audit logging: Every change made by an AI assistant is tracked under Charitable » Tools » Logs, so you always know who (or what) updated your site.

Check out our announcement here.

Improvement Payments

💰 Accept Recurring Donations with Windcave and Charitable

A donor sets their gift up once on Windcave’s secure payment page, and Charitable bills every renewal after that on schedule. Why this is important:

🏦 Organizations banked in New Zealand or Australia: monthly giving on the gateway your bank already set you up with, with no second processor to onboard.
⛪ Churches taking regular tithes and offerings: congregants set their own schedule once, which is the simplest way to launch recurring church giving without a separate platform.
🌏 Groups with donors in several currencies: Windcave handles more than 20, so a supporter can give in the currency they actually hold.
📅 Operating funds rather than one-time campaigns: switch on Recurring Only mode and the one-off option disappears, so every gift to that campaign is a subscription.
🧾 Teams with no developer on staff: 3 sets of credentials pasted into a settings page, and no code anywhere.

Check out our announcement here.