# TEST offer emails

> Send one real staging offer email after explicit guest submission, with a canonical perk award and durable retry deduplication.


A TEST welcome offer can award a configured perk and send one real email with
`[STAGING]` at the start of its normal subject. This is an early-access operation:
use a matching API deployment and immutable SDK release that includes these helpers.
Source tests alone do not establish deployed availability or inbox delivery.

## Configure the offer

The collection must have a canonical welcome perk with authored email copy and an
immediate `lead.captured` → `DIRECT_ISSUE` award rule. DRAFT perks can be rehearsed
in TEST without enabling a production offer. The award-email renderer consumes
an already-issued code, its expiry, approved redemption URL and terms. It does
not invent a booking, join a membership, or issue another code while rendering.

The installation needs `offers.read` and `offers.write`, a TEST server key, the
exact registered collection-owned STAGING origin, and registered TEST recipients.
Publishable browser keys and LIVE credentials cannot call these operations.
Keep the server key in the server environment; the browser uses your same-origin
BFF. Never add a provider key to application code or use a private capture API.

## Add the server and Next handlers

```ts
import { createKismetClient } from '@kismet-tech/sdk/server';
import { createKismetGuestAuthNextHandlers } from '@kismet-tech/sdk/next';

const server = createKismetClient({
  apiKey: process.env.KISMET_SERVER_KEY,
  collection: 'your-collection',
  baseUrl: 'https://api.ksmt.app/v1',
});
const offerId = process.env.KISMET_OFFER_ID!; // configured canonical perk UUID
const handlers = createKismetGuestAuthNextHandlers({
  ...yourExistingGuestHandlerConfig,
  offers: { client: server.offers, offerIds: [offerId] },
});
```

Map GET `/api/kismet/guest/offers/[offerId]` to `handlers.offer(request, offerId)`
and POST `/api/kismet/guest/offers/[offerId]/captures` to
`handlers.captureOffer(request, offerId)`. The existing auth CSRF route remains
required. The handlers enforce configured offer IDs and exact site origins,
validate CSRF on POST, and derive `origin` from the authorized request URL.
Caller-supplied origin, environment, code and extra fields are rejected.

For direct server integrations, use `server.offers.get(offerId, { origin })`
and `server.offers.capture(offerId, { ...input, origin })`. Both operations send
the exact Origin header; capture also sends the matching body origin.

## Display the disclosure before submission

```ts
import { createKismetGuestBrowserClient } from '@kismet-tech/sdk/react';
const guest = createKismetGuestBrowserClient();
const offer = await guest.offers.get(offerId);
// Render offer.headline, offer.promise, offer.terms and offer.disclosure.text.
// Only the explicit submit handler below may capture the offer.
const result = await guest.offers.capture(offerId, {
  email,
  idempotencyKey: stableSubmissionKey,
  consent: { accepted: true, disclosureHash: offer.disclosure.hash },
});
```

Typing an email or signing in with Google does not submit the offer. The canonical
TEST disclosure explains that this sends one real staging email without LIVE
marketing enrollment or membership. Render that exact disclosure and send its
hash only after the guest chooses the submit action. A changed disclosure must
be displayed again before a fresh explicit submission.

Keep the same 16–200 character alphanumeric, hyphen or underscore idempotency key
and identical payload for retries, including after a timeout. Do not generate a
new key just because the response is uncertain. The API also deduplicates by
recipient, offer and installation, so changing keys cannot produce another send.

## Report the actual result

`email.status: 'accepted'` means the provider accepted the email. It does not prove
inbox delivery or redemption. `not_sent`, `pending`, `unknown` and `failed` are
separate outcomes; never present them as sent. Preserve the submission key when
checking again. The response includes `replayed` and the durable `captureId`.

The API records the canonical award event and capture/send state. Engagement
clicks and Google login are not award, delivery, redemption or marketing-consent
proof. Do not infer dashboard fulfillment counts from those events.

See [read the offer](https://developers.kismet.travel/api/reference/get-developer-offer.md) and
[capture the offer](https://developers.kismet.travel/api/reference/capture-developer-offer.md) for the REST contract.
