Dispatch Lifecycle Event

Fire a lifecycle event from your backend right now — creates survey invitations and returns the magic-link URLs so you can relay them via your own notification channel

Overview

Fire a LifecycleEventKind for one of your users. SeggWat looks up every Live survey whose Backend trigger subscribes to that event, creates one SurveyInvitation per match, and returns the magic-link URLs in the response. Your backend is responsible for delivering those links to the user (email, SMS, in-product message, push, whatever fits) — SeggWat itself only renders the in-app widget; it does not send emails on your behalf.

Use this for "right-now" moments your backend already detects: a trial just expired, a subscription was cancelled, onboarding wrapped up. For future events (like a heads-up N days before trial expiry), use POST /api/v1/users/upsert with trial_ends_at instead — that one schedules; this one fires now.

Authentication

Authorization: Bearer <oat_token> or X-API-Key: <oat_token>.

Use an Organization Access Token. This endpoint runs from your backend, never the browser — origin headers don't apply to server-fired events, so API key auth is the only way to prove the request came from your org.

Request

POST /api/v1/lifecycle/dispatch

project_idstringrequired

The SeggWat project to fire this event against. Must belong to the API key's organization.

Example: "507f1f77bcf86cd799439011"

eventstring | objectrequired

Which lifecycle event to fire. One of the predefined snake-case strings, or {"custom": "your_event_name"} for a customer-defined event.

Dashboard checkbox API value
Trial expired "trial_expired"
Trial ending soon "trial_ending_soon"
Trial cancelled "trial_cancelled"
Subscription cancelled "subscription_cancelled"
Subscription downgraded "subscription_downgraded"
Onboarding completed "onboarding_completed"
First feedback submitted "first_feedback_submitted"
Custom event {"custom": "payment_failed"}

Example: "subscription_cancelled"

user_refstringrequired

Your stable identifier for the user this event applies to — the same user_ref you pass to users/upsert and SeggwatSurvey.setUser(...). Echoes back on SurveyResponse.submitted_by when the resulting invitation is completed, and on each entry of created_invitations in the response.

Example: "user_abc123"

Example requests

Right-now event with a predefined kind:

bash
curl -X POST https://seggwat.com/api/v1/lifecycle/dispatch \
  -H "Authorization: Bearer $SEGGWAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "507f1f77bcf86cd799439011",
    "user_ref": "user_abc123",
    "event": "subscription_cancelled"
  }'

Custom event name:

bash
curl -X POST https://seggwat.com/api/v1/lifecycle/dispatch \
  -H "Authorization: Bearer $SEGGWAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "507f1f77bcf86cd799439011",
    "user_ref": "user_abc123",
    "event": { "custom": "payment_failed" }
  }'

Pair this with a survey whose trigger lists {"custom": "payment_failed"} in the dashboard — the dispatcher matches by exact event equality.

Response

matched_surveysinteger

Number of Live surveys whose Backend event trigger matched the fired event. Always the number of surveys you've configured for that event.

created_countinteger

Number of new SurveyInvitation rows actually persisted. Equals created_invitations.length. Lower than matched_surveys when some matches were skipped by cooldown, sample rate, or errored.

suppressed_by_cooldown_countinteger

Matched surveys skipped because the per-survey cooldown window for this user_ref is still open. Most commonly the same event fired twice in quick succession — safe to ignore as long as it's not consistently equal to matched_surveys.

suppressed_by_sample_countinteger

Matched surveys skipped because the per-survey sample_rate roll missed. Independent of cooldown — a sample miss doesn't write an invitation, so a re-fired event gets a fresh roll.

errored_countinteger

Matched surveys that errored mid-dispatch (DB write fail, etc.). A non-zero value here is worth alerting on customer-side — the call succeeded but at least one invitation was lost.

created_invitationsarray

One entry per invitation actually created. Use the magic_link_url to deliver the survey to your user via your own notification pipeline.

  • survey_id (string) — id of the parent survey.
  • user_ref (string) — your user identifier, echoed back so the same field maps 1-to-1 to the dispatch request.
  • magic_link_url (string) — direct link to the SeggWat-hosted respond page. The token is single-survey and uniquely identifies this invitation — treat it like a password and only ship it to the targeted user.
  • expires_at (ISO 8601 timestamp) — after this point the link 410s. Default lifetime is 30 days; send well before then.

Example response

json
{
  "matched_surveys": 2,
  "created_count": 2,
  "suppressed_by_cooldown_count": 0,
  "suppressed_by_sample_count": 0,
  "errored_count": 0,
  "created_invitations": [
    {
      "survey_id": "65b1f00000000000000000aa",
      "user_ref": "user_abc123",
      "magic_link_url": "https://seggwat.com/surveys/abc...43chars",
      "expires_at": "2026-06-26T11:22:00Z"
    },
    {
      "survey_id": "65b1f00000000000000000bb",
      "user_ref": "user_abc123",
      "magic_link_url": "https://seggwat.com/surveys/def...43chars",
      "expires_at": "2026-06-26T11:22:00Z"
    }
  ]
}

Status codes

Code Meaning
200 Dispatch handled. Inspect the counts to see how many invitations were actually created.
400 Validation failed (user_ref empty, body unparseable, unknown event variant).
401 Missing or invalid API key.
403 API key's org doesn't own the project.
404 Project not found.

A 200 response is not the same as "the survey fired." Always check created_count to confirm at least one invitation was persisted. A 200 with matched_surveys: 0 means the call succeeded but no Live survey is subscribed to that event yet — most often a stale dashboard config.

The in-app widget surfaces pending invitations on the user's next session automatically — no extra work needed there. To reach users who aren't currently in the product, hand the magic_link_url to your existing notification stack:

python
# Example: relay the magic link via Postmark
import requests

dispatch = requests.post(
    "https://seggwat.com/api/v1/lifecycle/dispatch",
    headers={"Authorization": f"Bearer {SEGGWAT_API_KEY}"},
    json={
        "project_id": PROJECT_ID,
        "user_ref": user.id,
        "event": "subscription_cancelled",
    },
).json()

for invitation in dispatch["created_invitations"]:
    postmark.send_email(
        From="feedback@yourapp.com",
        To=user.email,
        Subject="Quick favour before you go — 2 questions",
        HtmlBody=render_template("survey_email.html", url=invitation["magic_link_url"]),
    )

This keeps you in full control of sender domain, deliverability, branding, unsubscribe handling, and the legal basis for the message.

When to call this

  • At the moment your backend detects the event. Your trial-expiry cron, your Stripe/Polar webhook handler, your onboarding-completion code path. SeggWat does not automatically dispatch from billing webhooks — your code must.
  • Once per event occurrence. The endpoint is not idempotent on its own; firing twice creates two invitations (unless the per-survey cooldown swallows the second). Dedupe at your caller if your detection code might re-emit.

Don't call this for

  • trial_ending_soon — use users/upsert with trial_ends_at. SeggWat schedules and de-schedules for you, including handling trial extensions and cancellations.
  • Pageview-style triggers — use the frontend-trigger mode (URL pattern or SeggwatSurvey.trigger("…")). Backend dispatch is overkill for "fire when the user lands on /pricing."
Navigation