Skip to content
KismetKismetDevelopers
llms.txt

Guest preferences and membership

View .md

Let 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.

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.

  1. Configure the same-origin guest-account integration.
  2. Start with a TEST installation with the collection-wide guest_preferences.write grant. The separate guest_auth.write grant 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.
  3. Register your site’s exact origin, such as https://preview.example.com or http://localhost:3000. An empty origin list does not permit capture.
  4. 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.

Use the same handlers instance returned by createKismetGuestAuthNextHandlers from @kismet-tech/sdk/next for sign-in and these two routes:

app/api/kismet/guest/preferences/route.ts
import { handlers } from '@/lib/kismet-auth';
export const GET = handlers.preferences;
app/api/kismet/guest/preferences/captures/route.ts
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.

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.

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 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 PENDING and 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.

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 },
);
Terminal window
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.

  • 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_CHANGED or PREFERENCES_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.