Guest preferences and membership
View .mdLet visitors explicitly sign up for email updates after identity verification, or manage email marketing and optional membership through Kismet-hosted preferences. Neither marketing nor membership is a condition of ordinary sign-in.
Inline email signup
Section titled “Inline email signup”For a signup form, show the collection’s email consent text next to the explicit signup action. After Google verifies the guest, record that choice without another confirmation email. For an email form, first complete email-link verification, then record the same choice. Signing in alone does not grant marketing consent.
The inline signup extension requires the matching API and SDK release and the
collection-wide guest_preferences.write grant. It does not enroll membership.
In TEST it records an isolated rehearsal only; it does not create LIVE campaign
eligibility or send an extra email. Existing hosted preference management below
continues to use its own confirmation flow.
Mount handlers.signupDisclosure as GET
/api/kismet/guest/preferences/signup-disclosure, and
handlers.emailSubscription as POST
/api/kismet/guest/preferences/email-subscriptions, using the same Next.js
handler instance as your guest sign-in routes.
import { createKismetGuestBrowserClient } from '@kismet-tech/sdk/react';
const signupGuest = createKismetGuestBrowserClient();const terms = await signupGuest.preferences.getSignupDisclosure();// Render terms.disclosure.text, privacyUrl, and any non-null notice.// Capture the explicit choice, version, hash and a unique idempotency key// before starting sign-in. Preserve them in your authenticated continuation.// After successful Google or email-link verification:async function finishSignup(verifiedGuestId: string, originalIdempotencyKey: string) { return signupGuest.preferences.subscribeEmail({ accepted: true, expectedGuestId: verifiedGuestId, disclosureVersion: terms.disclosure.version, disclosureHash: terms.disclosureHash, idempotencyKey: originalIdempotencyKey, });}Do not call finishSignup from a generic login callback without preserved signup
intent. Use the guest ID returned by verification, not a form-supplied identity.
An account switch returns GUEST_IDENTITY_CHANGED; stale terms return
DISCLOSURE_CHANGED. Ask for a fresh choice in either case. Retry the same
request with its original idempotency key. A replay returns the current preference
state and never reverses a later unsubscribe. Global email suppression is preserved.
The server client exposes getSignupDisclosure() and
subscribeEmail(input, { guestAccessToken, csrfToken }). It derives parentOrigin
from its configured origin. Direct REST uses
GET signup disclosure and
POST email subscription, with
the server key, verified guest token and CSRF proof held by your backend.
When combining signup with an offer, separately display the offer’s
deliveryDisclosure.text and use its hash for offer capture. If that field is
absent, do not enable combined signup against that API release. Offer delivery
consent never creates marketing permission by itself.
Set up hosted preference management
Section titled “Set up hosted preference management”- Configure the same-origin guest-account integration.
- Start with a TEST installation with the collection-wide
guest_preferences.writegrant. The separateguest_auth.writegrant permits guest sign-in, but does not authorize preference operations. If your dashboard’s capability picker does not yet show it, an authorized collection administrator or developer can create the installation through Kismet’s signed-in User MCP developer-access tools. - Register your site’s exact origin, such as
https://preview.example.comorhttp://localhost:3000. An empty origin list does not permit capture. - Keep the server key and verified guest access token in your backend. The browser uses the SDK guest client with the existing HttpOnly session and CSRF flow.
Wire the Next.js routes
Section titled “Wire the Next.js routes”Use the same handlers instance returned by createKismetGuestAuthNextHandlers
from @kismet-tech/sdk/next for sign-in and these two routes:
import { handlers } from '@/lib/kismet-auth';export const GET = handlers.preferences;import { handlers } from '@/lib/kismet-auth';export const POST = handlers.preferenceCapture;The capture handler validates the session, site origin, and CSRF proof. It derives
parentOrigin from the request site, not browser-supplied JSON. Responses are
private and non-cacheable.
Open the Kismet confirmation
Section titled “Open the Kismet confirmation”import { createKismetGuestBrowserClient } from '@kismet-tech/sdk/react';
const guest = createKismetGuestBrowserClient();const before = await guest.preferences.get();const capture = await guest.preferences.prepareCapture();
// Render a user-activated link to capture.confirmationUrl, for example:// <a href={capture.confirmationUrl} target="_blank" rel="noopener noreferrer">// Continue to Kismet// </a>// After the visitor returns, refresh the authoritative state:const after = await guest.preferences.get();Prepare the URL in response to the visitor’s intent to manage preferences, not on every page view. It expires after at most ten minutes (or earlier if the guest access proof expires) and is a bearer link. Do not log, cache, share, or put it in analytics. It opens in a separate tab or popup, not an iframe. The SDK validates that it points to Kismet’s confirmation origin and path.
For a popup integration, completion emits kismet:guest-preferences:complete
with captureId. Accept it only when event.origin is https://api.ksmt.app,
event.source is your opened popup, and captureId matches your capture. The
message is a refresh signal, never proof of consent. Re-read preferences through
your backend. A new-tab integration can simply refresh when the visitor returns.
What the visitor can try
Section titled “What the visitor can try”| Choice | Saved result |
|---|---|
| Keep current choice | No marketing change; no enrollment unless membership is separately selected |
| Join optional membership only | Membership becomes ACTIVE; email marketing is unchanged |
| Subscribe only | Email becomes PENDING; membership is unchanged |
| Confirm the email code | Pending email becomes SUBSCRIBED; TEST uses the displayed 000000, LIVE requires the emailed code |
| Unsubscribe | Email becomes UNSUBSCRIBED; membership is unchanged |
| Close before saving | No change; sign-in remains available |
Membership is offered only when the collection has an enabled membership program.
The simulated second step proves the interaction, not email deliverability or a
LIVE double-opt-in system. Never use TEST ACTIVE or SUBSCRIBED to authorize
real benefits or messages.
Use LIVE deliberately
Section titled “Use LIVE deliberately”Use a separate LIVE installation after completing the TEST checks. The same SDK
methods and BFF routes apply; neither the browser nor a query flag selects the
environment. Read state.environment and state.deliveries from the response.
- Membership only: creates or reactivates the collection’s free membership, with canonical member-benefit processing. It does not opt into marketing or trigger a promotional welcome email. Paid memberships are not supported here.
- Subscribe only: records
PENDINGand queues one confirmation email. It does not enroll the guest. The Kismet page accepts the code only after the delivery service has accepted the email, and only while the capture remains valid. - Unsubscribe: records the withdrawal without cancelling membership. An older confirmation cannot reverse a later withdrawal. A fresh subscription requires a new confirmation.
- Existing suppression: an address blocked for delivery stays blocked. This flow does not remove bounce or complaint suppression.
The Kismet page shows queued, sent, or unavailable delivery. If a provider result
is uncertain, the system does not automatically resend or mark the guest as
subscribed. The visitor can start a new confirmation. state.deliveries: "LIVE"
identifies the environment, not proof of inbox delivery; PENDING is not permission
to send marketing. Permission is collection-specific and bound to the verified
email address. Changing the address does not transfer the prior opt-in.
LIVE preferences are shared across that collection’s installations for the same guest. TEST preferences remain installation-isolated. The read operation never enrolls or subscribes anyone. SMS, payment, and booking actions are separate.
REST and server SDK
Section titled “REST and server SDK”The same operations work with any server framework. A server client created with
createKismetGuestAuthServerClient from @kismet-tech/sdk/server provides:
const preferences = await client.getPreferences(guestAccessToken);const capture = await client.preparePreferenceCapture( { parentOrigin: 'https://preview.example.com' }, { guestAccessToken, csrfToken },);curl 'https://api.ksmt.app/v1/developer/guest/me/preferences' \ -H "Authorization: Bearer $KISMET_SERVER_KEY" \ -H "X-Kismet-Guest-Token: $GUEST_ACCESS_TOKEN"
curl 'https://api.ksmt.app/v1/developer/guest/me/preferences/captures' \ -X POST -H 'Content-Type: application/json' \ -H "Authorization: Bearer $KISMET_SERVER_KEY" \ -H "X-Kismet-Guest-Token: $GUEST_ACCESS_TOKEN" \ -H "X-Kismet-CSRF: $VALIDATED_CSRF_TOKEN" \ --data '{"parentOrigin":"https://preview.example.com"}'Your backend must validate CSRF and the request origin before forwarding a capture request. Neither operation accepts an arbitrary guest ID, membership ID, or consent flag. The guest token determines whose preferences are accessed.
See GET guest preferences and
POST prepare preference capture
for the complete request and response contracts. Response state includes
environment, revision, emailMarketing, membership.status,
membership.joinedAt, and deliveries. disclosureHash identifies the displayed
disclosure; it is not a consent receipt or an authorization token.
Handle expiry and changed state
Section titled “Handle expiry and changed state”401: sign in again; do not substitute a guest ID from the browser.403: check the explicit preference grant, server credential, and registered origin. Revoking the creator credential, installation, grant, or origin also invalidates prepared confirmations.404 CAPTURE_UNAVAILABLE: the link is invalid or expired; prepare a new one.409 DISCLOSURE_CHANGEDorPREFERENCES_CHANGED: refresh state and open a new confirmation. A stale subscription confirmation cannot reverse a withdrawal.409 CAPTURE_CONFLICT: the same capture already recorded different choices.409 EMAIL_SUPPRESSED: leave the current choices unchanged; do not work around the delivery block.409 CAPTURE_SCOPE_CHANGED: the signed-in account or email changed; prepare a new confirmation.429: wait before retrying. Capture preparation is bounded to ten captures per guest/installation in TEST or guest/collection in LIVE in ten minutes, in addition to the request-rate limit. Five incorrect LIVE codes lock that capture.503: temporary unavailability; leave the existing state unchanged.
Confirmations are replay-safe: repeating the same submitted choice does not create another receipt or enrollment. Keep the prior result visible during a failed refresh and clearly distinguish unknown state from unsubscribed state.