Skip to main content

FAQ: per-user state, share-link revocation, and send-time experiments

This page answers the questions that come up once your team starts relying on per-user state (saved drafts, signatures, reports), permanent share links, password resets, and send-time experiments. Each answer states the observable behavior and the API call behind it.

Why did my draft disappear when I switched browsers?

Saved AI campaign drafts used to live in browser storage, so a draft saved on one machine was invisible on another device, in another browser, or after clearing site data. Drafts now sync to your user account: the dashboard stores and loads them with GET /api/v1/me/campaign-drafts and PUT /api/v1/me/campaign-drafts, and the saved list follows you across browsers and devices. If a draft shows up in one browser but not another, one of the sessions saved it locally before the sync existed. Open the newer session and save the draft again; it then reads back everywhere you sign in. Drafts are private to the user who saved them, capped at 50 drafts and a total size limit per user. Call:
The response is {"id": "<file_id>", "revoked": true}, and the revocation is written to your organization’s audit trail as a file.share.revoked event. How it behaves:
  • Revocation rotates the share_nonce held on the stored object. Every link minted before the rotation stops resolving on the next open; there is no grace window.
  • Minting is deterministic per file: the payload carries no issue time, so re-minting the share for the same file returns the same URL while the nonce is current. Minting twice before a revocation is a no-op, and the link you filed with the carrier stays byte-identical to the one the dashboard shows.
  • Share links carry no expiry by design (a carrier reviewer may open the link weeks after submission), so revocation is the only way to withdraw one. Rotate the nonce as soon as the carrier no longer needs the link.

What happens to user API keys after a password reset?

Only a reset minted through the forgot-password / reset chain triggers the post-reset key kill. When a member completes that flow, the reset response hands the dashboard a short-lived follow-up token and the dashboard offers to revoke the member’s existing API keys in one call. That is the only path that revokes keys. A routine password change in account settings (PATCH /me) does not revoke API keys. If keys keep working after a routine change, that is the designed behavior, not a missed revocation. Reset links are one-shot, and a link that cannot be consumed is burned, not left half-open:
  • A corrupt reset payload (a link mangled in transit or by a mail scanner) is discarded and the stored row is burned so the link can never be replayed.
  • A token that is well-formed but does not match the stored row (already used, or minted for a different state) is likewise burned.
Both rejections are recorded in the platform security audit trail as alertable burn events carrying a reason (corrupt_token_row for the corrupt case, a mismatch reason otherwise) and a redacted key suffix, never the token itself. If you see a member reporting “my reset link did nothing,” the matching burn event is the thing to look for: it tells you the link was rejected and disposed of, and that the member needs a fresh reset email.

How is my optimal send-time arm assignment computed, and does it leak across campaigns?

When a campaign opts in to a send-time experiment (2-4 competing send windows plus an optional do-nothing control), each contact is hashed into a bucket in [0, 9999]:
The first holdout_pct percent of the bucket space is the control arm; the remainder is split evenly across the configured windows. Two guarantees follow from that seed:
  • Stable within a campaign. The assignment depends only on the two ids, so a contact’s arm is identical across every pause, resume, and audience re-resolution. Resuming a paused campaign never moves a contact between arms.
  • Uncorrelated across experiments. The send-time-experiment: prefix makes these buckets statistically independent of the platform’s other experiment hashes (the per-variant content A/B bucket and the campaign-level control holdout). A contact’s send-time arm cannot bias any other assignment, and an assignment never leaks from one campaign into another because the campaign id is part of the seed.
The bucket space is fine-grained (10,000 buckets) precisely so that up to four windows split evenly and an operator can predict a cohort: given the two ids you can recompute any contact’s arm offline.

What state does the platform keep per user?

Per-user preferences are stored as a JSONB preferences object on the user row. The keys the platform writes today: All keys are per user, not per organization, and hold interface and drafting state only, never contact or message data. Every write merges a single key into the object, so saving a draft cannot clobber your signature or reports.

Where do I see my per-contact next-best-channel?

The smart-send preview response carries an advisory next_best_channel block next to the recommended send times, naming the channel the contact is predicted to respond on. The block appears only when the campaign declares two or more channels; with a single declared channel there is nothing to compare, so the block is omitted from the response.

Can I opt out a contact from timing experiments?

Use the standard audience exclusions you already apply for suppression: an excluded contact never enters the experiment’s matched audience, so they receive neither a window arm nor the control. There is no per-experiment opt-out flag; exclusion is the control. The control arm itself is assigned by the same fixed hash, with the first holdout_pct percent of the bucket space reserved for it. The same contact therefore always resolves the same way for a given campaign, in control or in a window, on every run.

Operator runbook

  • Withdraw a filed RCS share link: DELETE /api/v1/files/{file_id}/share. Confirm revoked: true in the response and the file.share.revoked entry in the audit trail. Old links 404-equivalent on next open; no further action is needed.
  • Member says a reset link did nothing: look for the burn event in the security audit trail (reason corrupt_token_row or a mismatch reason, redacted key suffix). It means the link was rejected and burned; issue a fresh reset email. A routine password change will not have revoked anything, so do not read working keys as a reset failure.
  • Explain an experiment cohort to a stakeholder: arms come from the fixed hash above; they are stable across pause/resume and uncorrelated with other experiments by construction.
For related behavior, see Auth and API keys and Password rejected during password reset.