Skip to article
Documentation menu

Participation

Moments and asks

Send a trusted event, show a timely ask and report the outcome.

Configure a moment you can verify

In Programme, save an enabled positive moment named project_completed. A moment is a business event your server knows happened, not a generic page view.

Names match exactly, including case. Use 1–80 characters, starting with a letter; letters, numbers, spaces, underscores, dots and hyphens are allowed. Negative moments record feedback and suppress asks instead of asking.

Project completed, teammate invited or NPS 9–10 are events you supply. ReferSprout does not automatically watch your product or run an NPS integration. Default wording uses your actual reward, milestone and hold; if you customise it, keep it accurate when terms change. Wording is plain text, never HTML.

Send the event from your server

First enrol the customer. Then call POST /api/v1/moments after authenticating them and confirming the event in your own system. This example uses receipt time so a copied example cannot accidentally become stale.

Server · cURL
curl https://refersprout.com/api/v1/moments \
  -H "Authorization: Bearer $REFERSPROUT_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"customer_id":"cus_example","moment":"project_completed","event_key":"project:123:completed:v1"}'
FieldRule
customer_idYour trusted server-side Stripe customer mapping; already enrolled and verified again in the connected account.
momentExact saved definition name.
event_keyStable ID for one business event, 1–128 ASCII letters/numbers or _.:-. Unique across your account/mode, not just this customer.
occurred_atOptional ISO timestamp. Defaults to receipt. Within the past 7 days or up to 5 minutes in the future; future values are clamped to receipt time.
account_created_atOptional trusted account creation time from your server. Omit when unknown. Future dates are rejected. The first known age persists.

Send only these fields. Unknown fields are rejected; the JSON body is limited to 4,096 bytes. Do not attach raw customer payloads or arbitrary event metadata.

Retry the same event key after an ambiguous response. A retry returns the original decision, not a new ask. Reusing it for another customer or moment returns event_key_conflict.

Read ask or skip before rendering

A new event returns HTTP 201, a duplicate HTTP 200. IDs, tokens and times are generated per decision.

201 · JSON (approved decision)
{
  "decision_id": "00000000-0000-4000-8000-000000000002",
  "created": true,
  "action": "ask",
  "reason": null,
  "help": null,
  "expires_at": "2026-10-27T12:02:00.000Z",
  "report_token": "SIGNED_REPORT_TOKEN",
  "wording": "Give a friend 20% off one invoice. You can earn $10.00 account credit after 2 qualifying payments, with a 14-day hold.",
  "view": {
    "referrer_id": "00000000-0000-4000-8000-000000000001",
    "code": "SAM8K2QX",
    "link": "https://merchant.example/join?prefilled_promo_code=SAM8K2QX&client_reference_id=rs_SAM8K2QX",
    "rewards": {
      "you_get": "$10.00 account credit",
      "friend_gets": "20% off one invoice",
      "friend_reward_type": "percent_off",
      "after_payments": 2,
      "hold_days": 14
    },
    "progress": {
      "friends_joined": 0,
      "friends_qualified": 0,
      "rewards_earned": 0,
      "rewards_issued": 0
    }
  }
}

When action is skip, do not show a prompt. view, wording and report_token are null. The response includes a reason and actionable help:

201 · JSON (skipped decision)
{
  "decision_id": "00000000-0000-4000-8000-000000000003",
  "created": true,
  "action": "skip",
  "reason": "recent_copy",
  "help": "Wait until the quiet period after copying ends.",
  "expires_at": "2026-10-27T12:02:00.000Z",
  "report_token": null,
  "view": null,
  "wording": null
}

A positive event over five minutes old is stored but skipped as stale_event. A delayed negative event starts its quiet period at receipt, not at its old event time. See all skip reasons.

Report what was actually shown or copied

An approved decision reserves a slot for 120 seconds. It does not count as shown yet. Render only a current approved decision and call POST /api/v1/ask-reports on actual presentation. Hide or cancel the prompt if the report is refused.

Server · cURL (report token, not API key)
curl https://refersprout.com/api/v1/ask-reports \
  -H 'Content-Type: application/json' \
  -d '{"token":"SIGNED_REPORT_TOKEN","outcome":"shown"}'
200 · JSON (first shown report)
{
  "accepted": true,
  "changed": true,
  "context_token": null
}

In your own frontend, forward real outcomes through your own same-origin server route to this endpoint. Do not assume cross-origin browser requests to ReferSprout are enabled. Your route must preserve the signed token, not accept an arbitrary customer ID.

Send outcome: "copied" only after a successful copy, and outcome: "dismissed" when dismissed. A first dismissal also counts as shown; copy requires an accepted shown or dismissed report first. Retried reports are deduplicated: changed: false means it was already recorded.

Shown, copied and dismissed each count at most once per decision. Reports recheck current customer participation, moment, pause, negative feedback and ask rules. A stale reservation cannot bypass a new negative event or deactivation.

The report token lasts 15 minutes from decision creation; this does not extend the two-minute deadline for the first shown/dismissed report. A duplicate consumed or expired event returns decision_used_or_expired, never a new reservation. Only a genuinely new business event may request another decision.

Tokens are bearer capabilities. Keep report tokens out of URLs, logs, analytics and referrer headers. Do not substitute a widget or hosted session token. Reporting an action never issues a reward.

Carry optional prompt context

Built-in prompts are hosted by ReferSprout in the widget/SDK frame. The resolver prepares signed context before the first clipboard write. Preparation records no copy, visit or attribution; only a successful copy with an accepted copied report activates it. Failed copying or a refused report does not. The SDK sends the report token through origin-checked postMessage, then the frame posts it to the resolver, keeping the report bearer out of URLs. The copied link adds optional ask context, not a Stripe customer ID or the report token.

For custom API integrations, an accepted copied report returns context_token, valid for 30 days from decision creation. A duplicate copy returns the same token. Your integration can carry this as rs_ask on the shared landing URL, preserving client_reference_id and any prefilled promotion code.

On the merchant landing page, POST /api/v1/ask-visits with {"context_token":"SIGNED_CONTEXT_TOKEN"} records at most one observed visit per copied decision. HTTP 200 returns {"changed":true}; duplicates return false. This is not a visitor count.

Your authenticated server may pass refersprout_ask in Checkout Session, subscription or invoice metadata. Or attach it after the referral webhook through:

Server · cURL
curl https://refersprout.com/api/v1/referral-context \
  -H "Authorization: Bearer $REFERSPROUT_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"customer_id":"cus_referred_example","context_token":"SIGNED_CONTEXT_TOKEN"}'

Here customer_id is the referred friend, not the referrer. HTTP 200 returns changed. A missing referral is 409 referral_not_found; retry after its webhook. Context only attaches when account, mode and winning referrer match, and the referral was created at or after the copy. It never changes first attribution, promotion-code priority or rewards.

Public Stripe Payment Links do not automatically turn arbitrary URL parameters into metadata. Without your propagation integration, prompt context can be missing. Code-only sharing has no visit context. Missing context is not proof that an ask had no influence.