Upsert User

Register or update one of your users so SeggWat can fire time-based lifecycle surveys at the right moment

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.

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_idstringrequired

The SeggWat project to scope this user against. Must belong to the API key's organization.

Example: "507f1f77bcf86cd799439011"

user_refstringrequired

A 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"

emailstring

The 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_atstring

ISO-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

bash
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

idstring

Internal SeggWat id for the persisted row. Stable across upserts of the same (project_id, user_ref).

project_idstring

Echoed back unchanged.

user_refstring

Echoed back unchanged.

emailstring

Current stored email, or null.

trial_ends_atstring

Current stored trial end timestamp, or null.

created_atstring

When SeggWat first saw this user.

updated_atstring

When this row was last upserted. Bumped on every successful call.

Example response

json
{
  "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.

bash
# 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.

Navigation