Overview
Tell SeggWat about a user in your system. Used primarily so we can schedule the Trial ending soon survey ahead of the user's trial expiry. The optional email is stored so the dashboard can show "feedback from alex@acme.com" alongside in-app responses — SeggWat does not deliver survey invitations by email itself (see lifecycle/dispatch for how magic-link URLs are returned for you to relay).
The endpoint is idempotent: calling it again with the same (project_id, user_ref) updates the existing row in place. Re-upserting with a different trial_ends_at cancels any previously-scheduled survey for that user and reschedules with the new value.
For an end-to-end walkthrough, see the Backend-triggered Surveys guide. This page is the formal API reference.
Authentication
Authorization: Bearer <oat_token> or X-API-Key: <oat_token>.
Use an Organization Access Token — these endpoints run from your backend, never the browser.
Request
POST /api/v1/users/upsert
project_idstringrequiredThe SeggWat project to scope this user against. Must belong to the API key's organization.
Example: "507f1f77bcf86cd799439011"
user_refstringrequiredA stable identifier for this user in your system. Whatever you use internally — database id, email, UUID — as long as you can pass the same value consistently. Echoes back as submitted_by on survey responses.
Example: "user_abc123"
emailstringThe user's contact email. Optional — stored so the dashboard can correlate in-app survey responses with an address. SeggWat does not send any email to this address.
Pass null (or omit) to clear a previously-stored email.
Example: "alex@acme.com"
trial_ends_atstringISO-8601 timestamp for when the user's trial expires. Setting this enables the Trial ending soon scheduler for this user; the survey will fire at trial_ends_at - warn_days (per-survey setting, default 3 days).
Pass null (or omit) when the user is no longer trialing — cancels any pending schedule.
Example: "2026-06-15T00:00:00Z"
Example request
curl -X POST https://seggwat.com/api/v1/users/upsert \
-H "Authorization: Bearer $SEGGWAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "507f1f77bcf86cd799439011",
"user_ref": "user_abc123",
"email": "alex@acme.com",
"trial_ends_at": "2026-06-15T00:00:00Z"
}'Response
idstringInternal SeggWat id for the persisted row. Stable across upserts of the same (project_id, user_ref).
project_idstringEchoed back unchanged.
user_refstringEchoed back unchanged.
emailstringCurrent stored email, or null.
trial_ends_atstringCurrent stored trial end timestamp, or null.
created_atstringWhen SeggWat first saw this user.
updated_atstringWhen this row was last upserted. Bumped on every successful call.
Example response
{
"id": "65f12a3b9c1e4d2f8a7b6c5d",
"project_id": "507f1f77bcf86cd799439011",
"user_ref": "user_abc123",
"email": "alex@acme.com",
"trial_ends_at": "2026-06-15T00:00:00Z",
"created_at": "2026-06-01T09:14:22Z",
"updated_at": "2026-06-01T09:14:22Z"
}Status codes
| Code | Meaning |
|---|---|
200 |
User upserted. The row was either created or updated. |
400 |
Validation failed (user_ref empty, project_id malformed, body unparseable). |
401 |
Missing or invalid API key. |
403 |
API key's org doesn't own the project. |
404 |
Project not found. |
Clearing values
Sending an explicit null distinguishes "clear this field" from "leave it alone." Today the endpoint applies the full payload — every call replaces email and trial_ends_at together — so to keep a field unchanged, send its current value back. The recommended pattern is to mirror your local user state on every upsert, not to compute deltas.
# Clear the trial — user converted or cancelled.
curl -X POST https://seggwat.com/api/v1/users/upsert \
-H "Authorization: Bearer $SEGGWAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "507f1f77bcf86cd799439011",
"user_ref": "user_abc123",
"email": "alex@acme.com",
"trial_ends_at": null
}'The next time the previously-scheduled survey would have fired, the worker re-checks the stored trial_ends_at, finds it cleared, and discards itself as a clean no-op — no extra cancellation call needed.
When to call this
- On signup — if the user starts in a trial, pass
trial_ends_at. If not, omit. - On trial extension or shortening — re-upsert with the new
trial_ends_at. - On conversion or cancellation — re-upsert with
trial_ends_at: null. - On email change — re-upsert with the new email.
Don't call this on every request or page load — once per state change in your own subscription / trial code is enough.
Related
- Backend-triggered Surveys guide — end-to-end walkthrough.
POST /api/v1/lifecycle/dispatch— the "right-now" companion endpoint for events with no future delay (trial_expired,subscription_cancelled, etc.).
