Skip to main content
Together
Sign in

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.

Sending introductions requires a Raise subscription. Receiving them is free for the invitee.

State machine

An introduction moves through six states. The webhook column shows the event that fires when the row enters that state.

StateMeaningWebhook fired on entry
pendingCreated, email sent, invitee hasn't opened the link.introduction.created
viewedInvitee landed on the public invite page.introduction.viewed
acceptedInvitee signed in (or signed up), confirmed the invitation, and bound it to one of their organisations.introduction.accepted
linkedInvitee 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)
expiredThe TTL passed without the invitee reaching linked. Cron sweeps hourly. Send a fresh introduction to retry.introduction.expired
revokedThe 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:

  1. Email lands in their inbox: subject {Inviter}would like to work with you on Together”, with a link to the invite landing.
  2. They click through to https://alltogether.giving/invite/<token>. That fires introduction.viewed.
  3. They sign up (or sign in) and either create a new organisation or pick an existing one they admin.
  4. They confirm the invite (fires introduction.accepted), then either link an existing eligible Stripe account or run Stripe Connect onboarding.
  5. On link, both introduction.linked and recipient_org_account_link.created fire. The link shows up in your /api/v1/recipient-accounts list and you can start allocating to it in recommendation sets.

Confidentiality contract

We're careful about what crosses the inviter / invitee boundary:

FieldVisible to inviteeVisible to inviter API
invitee_emailSent the email - they see their own address.Yes (it's your input).
invitee_org_name_hintYes, on the email + landing. Helps confirm it's for them.Yes.
invitee_messageYes, rendered on the email + landing.No - write-only. Not echoed in any GET.
tokenIn 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 expired and 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.