Introductions
Send a partner organisation a one-time link that lets them set up as a recipient on Together. Sign-up, Stripe Connect onboarding, and recipient-account linking all happen on their side - you just send the invite.
When to use this
Introductions are programmatic invitations from one fundraising org (the inviter) to another (the invitee). They're for the case where you want to recommend a partner to your donors but the partner isn't on Together yet.
For partners who are already on Together and have shared a distribution passphrase with you, use POST /api/v1/recipient-accounts directly - no introduction needed.
State machine
An introduction moves through six states. The webhook column shows the event that fires when the row enters that state.
| State | Meaning | Webhook fired on entry |
|---|---|---|
pending | Created, email sent, invitee hasn't opened the link. | introduction.created |
viewed | Invitee landed on the public invite page. | introduction.viewed |
accepted | Invitee signed in (or signed up), confirmed the invitation, and bound it to one of their organisations. | introduction.accepted |
linked | Invitee picked an eligible Stripe account; a RecipientOrgAccountLink exists between you and their account. The introduction is complete - distributions can flow. | introduction.linked + recipient_org_account_link.created (with introduction_id) |
expired | The TTL passed without the invitee reaching linked. Cron sweeps hourly. Send a fresh introduction to retry. | introduction.expired |
revoked | The inviter cancelled the introduction before it linked. Allowed at any non-terminal state. | introduction.revoked |
Send an introduction
curl -sS https://alltogether.giving/api/v1/introductions \
-H "Authorization: Bearer pc_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"invitee_email": "contact@partner.example.org",
"invitee_org_name_hint": "Riverbank Action Fund",
"invitee_message": "Hey - we'\''d love to direct some donations your way. Set up here so we can route them through.",
"ttl_days": 30,
"external_id": "crm-id-12345"
}'The response includes the introduction id and status but not the token - the token is bearer-grade and is delivered to the invitee via email only. The invitee_messageis similarly write-only on the API; we don't echo it back so a leaked API call doesn't surface what was sent.
external_id is your CRM-side identifier and is enforced unique per inviter. Use it to make POSTs idempotent on your side.
Resend, revoke, list
# Resend - capped at 3 per introduction. Only valid while pending or viewed. curl -X POST https://alltogether.giving/api/v1/introductions/intr_abc/resend_email \ -H "Authorization: Bearer pc_your_key_here" # Revoke - allowed at any non-terminal state. curl -X DELETE https://alltogether.giving/api/v1/introductions/intr_abc \ -H "Authorization: Bearer pc_your_key_here" # List your in-flight introductions, filtered by status. curl -sS "https://alltogether.giving/api/v1/introductions?status=pending&limit=20" \ -H "Authorization: Bearer pc_your_key_here"
What the invitee sees
The flow on the recipient side is:
- Email lands in their inbox: subject “{Inviter}would like to work with you on Together”, with a link to the invite landing.
- They click through to
https://alltogether.giving/invite/<token>. That firesintroduction.viewed. - They sign up (or sign in) and either create a new organisation or pick an existing one they admin.
- They confirm the invite (fires
introduction.accepted), then either link an existing eligible Stripe account or run Stripe Connect onboarding. - On link, both
introduction.linkedandrecipient_org_account_link.createdfire. The link shows up in your/api/v1/recipient-accountslist and you can start allocating to it in recommendation sets.
Confidentiality contract
We're careful about what crosses the inviter / invitee boundary:
| Field | Visible to invitee | Visible to inviter API |
|---|---|---|
invitee_email | Sent the email - they see their own address. | Yes (it's your input). |
invitee_org_name_hint | Yes, on the email + landing. Helps confirm it's for them. | Yes. |
invitee_message | Yes, rendered on the email + landing. | No - write-only. Not echoed in any GET. |
| token | In the landing URL. | No - never returned via API. Re-sending the email re-uses the same token. |
Accepting user's identity (accepted_by_user_id / linked_by_user_id) | That's themselves. | No- the invitee's admin identity is their org's business, not yours. |
Accepted organisation id (accepted_organisation_id) | It's their own org. | Yes once they accept - you need it to know which org accepted. |
Limits and abuse rails
- 50 PENDING per inviter org per rolling 24h. A soft anti-spam rail; a 51st POST in the window returns 400.
- 3 resends per introduction. 4th call returns 400. Send a fresh introduction instead.
- TTL: 1-90 days, default 30. Past expiry the hourly cron flips the row to
expiredand fires the webhook. - Sandbox orgs can't accept.If the invitee tries to accept in sandbox context they get bounced to live - sandbox doesn't support real Stripe Connect, so the link step would fail downstream.
- Inviter can't self-accept.A user belonging to the inviter org clicking their own invite link is shown a friendly “that's your own introduction” page; the action layer also refuses to bind.
Webhooks at a glance
Subscribe to introduction.* events on a webhook endpoint to drive your CRM. The headline events are introduction.linked and recipient_org_account_link.created; the latter carries an introduction_id field so you can correlate the two.
Full payload schemas are on the webhooks reference. Delivery semantics, signature verification, and replay handling are documented there too.