# Developer events and signed webhooks

> Read durable installation events, configure signed HTTPS delivery, verify raw bytes and replay safely.


Connect your application's workflows to durable Kismet events. Read an installation's event history, register an HTTPS destination, inspect delivery status, and replay a retained event without recreating the underlying business action.

> **Early access**
>
> Prepare and test this integration with an evaluation SDK and API environment. Production event delivery is not yet enabled. [Request access](mailto:engineering@makekismet.com?subject=Developer%20events%20evaluation).

Existing catalog APIs and collection webhooks are unchanged.

## Set up access with OAuth

Connect the [Developer MCP](https://developers.kismet.travel/mcp.md#authentication-and-access) in your coding client and choose **Sign in with Kismet**. An `ADMIN` or `DEVELOPER` on the collection can create the application's event/webhook grants and issue its server credential through MCP. Dashboard key copying is not required.

Ask your agent:

> Set up a separate TEST installation for event and webhook evaluation on `your-collection`. Reuse an appropriate application if one exists. Preview `events.read`, `webhooks.read`, `webhooks.write`, `payment_methods.events.read`, and `guest_wallet_requests.events.read`, with no browser origins and no other grants. Show me the exact scope before confirming. Establish a secure server secret destination before issuing a `SERVER_SECRET`; never display its value in chat. Then prepare the signed HTTPS receiver and register it using the server SDK.

Follow the [OAuth setup sequence](https://developers.kismet.travel/guides/developer-access.md#set-up-through-your-coding-agent): `list_developer_applications`, create/activate an application if needed, then preview and confirm `create_developer_installation`. Use `environment: "TEST"`, `origins: []`, and the grants below. Omit `entity_id` because these grants are collection-wide. After approval, use `issue_developer_credential` with `kind: "SERVER_SECRET"` and the returned installation ID.

Confirm the client can securely store the once-only result before issuance. Kismet does not automatically write to Lovable or another host's secret store. The agent must use an authorized server-side storage action, not paste the key into the conversation.

OAuth configures access. Your application then uses the installation credential for event reads, destination creation, delivery inspection, and replay through the SDK/REST operations below. Those operations are not currently OAuth MCP management tools. The collection-manager `manage_webhooks` tool manages a different collection-level service; it is not a substitute for these installation-scoped subscriptions.

### Required grants

Create a separate TEST installation for evaluation. Its collection-wide grants must explicitly include:

| Capability | Purpose |
| --- | --- |
| `events.read` | Read retained events |
| `webhooks.read` | Read destinations and delivery status; allow delivery |
| `webhooks.write` | Create, enable/disable destinations, and request replay |
| `payment_methods.events.read` | Read and receive `payment_method.added` |
| `guest_wallet_requests.events.read` | Read and receive `guest.wallet_request.completed` |

Select only the event families your application needs. A guest-auth or wallet-request write grant does not imply these permissions. Publishable keys and guest access tokens cannot use this API.

Kismet derives application, installation, collection, and TEST/LIVE environment from the server credential. There is no collection or environment override in an event request. A TEST installation never reads LIVE events.

## Create a destination

```sh
curl --request POST "$KISMET_API_BASE/developer/webhook-subscriptions" \
  --header "Authorization: Bearer $KISMET_DEVELOPER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Application events",
    "url": "https://builder.example/webhooks/kismet",
    "eventTypes": ["payment_method.added", "guest.wallet_request.completed"],
    "envelopeVersion": "2"
  }'
```

`KISMET_API_BASE` includes `/v1`. Configure the supplied evaluation API URL while this contract is in preview.

The response contains `subscription` and a **reveal-once `signingSecret`**. Store the secret in server configuration before discarding the response. List and update operations return a hint, not the secret. The destination must be public HTTPS without URL credentials or fragments. Redirects are not followed, and destination safety is checked again on dispatch.

Equivalent typed SDK calls:

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

const client = createKismetClient({
  apiKey: process.env.KISMET_DEVELOPER_API_KEY!,
  baseUrl: process.env.KISMET_API_BASE!,
});

const created = await client.webhookSubscriptions.create({
  name: 'Application events',
  url: 'https://builder.example/webhooks/kismet',
  eventTypes: ['payment_method.added', 'guest.wallet_request.completed'],
  envelopeVersion: '2',
});
// Persist created.signingSecret in your server's secret store.
// Retain created.subscription.id and its four scope fields.
```

These facades are available on the server entry point's `createKismetClient`. They do not require a catalog lookup or a `collections.read` grant. Existing catalog methods remain available on the same client.

## Read, inspect, and replay

```ts
const page = await client.events.list({
  type: 'payment_method.added',
  limit: 25,
});
const next = page.nextCursor
  ? await client.events.list({
      type: 'payment_method.added',
      after: page.nextCursor,
      limit: 25,
    })
  : null;

const event = await client.events.get(eventId);
const destinations = await client.webhookSubscriptions.list();
const deliveries = await client.webhookSubscriptions.deliveries(subscriptionId);
const replay = await client.events.replay(eventId, subscriptionId);
await client.webhookSubscriptions.update(subscriptionId, { enabled: false });
```

`eventId` and `subscriptionId` above are IDs returned by Kismet, not names or slugs.

| Operation | SDK |
| --- | --- |
| GET `/developer/events` | `client.events.list(options)` |
| GET `/developer/events/{eventId}` | `client.events.get(eventId)` |
| GET `/developer/webhook-subscriptions` | `client.webhookSubscriptions.list()` |
| POST `/developer/webhook-subscriptions` | `client.webhookSubscriptions.create(input)` |
| PATCH `/developer/webhook-subscriptions/{subscriptionId}` | `client.webhookSubscriptions.update(id, { enabled })` |
| GET `/developer/webhook-subscriptions/{subscriptionId}/deliveries` | `client.webhookSubscriptions.deliveries(id, options)` |
| POST `/developer/events/{eventId}/replay` | `client.events.replay(eventId, subscriptionId)` |

Event and delivery pages use `after` and `nextCursor`, default limit 25 and maximum 100, ordered by recording time then ID. Filters and current grants apply before pagination. Keep the same type filter when passing a cursor. A foreign or no-longer-authorized cursor returns `400 INVALID_CURSOR`.

An unfiltered event list includes only currently authorized families. An explicitly requested ungranted type returns `403 CAPABILITY_NOT_GRANTED`; a missing, foreign, or no-longer-granted event detail returns `404 NOT_FOUND`.

Events persist even when no destinations exist. A new subscription receives future fan-out, not an automatic historical backfill. Read retained events and request replay explicitly. Replay keeps the same event ID and body. `queued: false` means an attempt is already sending; it does not mean the event was lost.

## Event meaning

| Type | Meaning | Data |
| --- | --- | --- |
| `payment_method.added` | A new saved method was confirmed for the event's environment | `payment_method_id`, nullable `request_id`, nullable `reference` |
| `guest.wallet_request.completed` | A wallet request completed after canonical capture confirmation | `request_id`, `payment_method_id`, `outcome`, nullable `reference` |

Completion outcome is `added` or `already_present`. An already-present method can complete a request without a new `payment_method.added` event.

In LIVE, confirmation persists the method in the guest's canonical Kismet wallet. In TEST, it records an isolated test capture and never adds a method to the LIVE wallet. Check both `environment` and `livemode`; a TEST event is not evidence that a real payment method is available.

Events are recorded only after authoritative capture confirmation commits, together with the capture and request outcome. Opening or closing the card form, receiving a browser message, or submitting a request does not prove completion. Repeated confirmation callbacks retain the original event IDs. A failure to persist the event also rolls back the completion, so a retry cannot leave an unrecorded successful transition.

The v2 envelope includes `id`, `type`, `version`, `api_version`, `created_at`, `occurred_at`, `timestamp`, `environment`, `livemode`, `source`, `collection`, `subject`, `object`, and `data`. `timestamp` is the occurrence-time compatibility field; the signed header timestamp is the delivery-attempt time.

`subject.id` is a persistent installation-scoped `dgs_…` guest reference, not a global guest-profile ID. Object IDs are Kismet references. No card details, payment-provider identifiers, contact details, guest tokens, or capture URLs are included. The optional correlation `reference` is the opaque request reference, up to 128 characters using letters, numbers, underscores, hyphens, dots, and colons. Do not put personal information in it.

**Neither event authorizes a charge, a booking, or marketing.** It records a completed fact. It does not prove that a method remains chargeable now. Use the separate authorized operation for any subsequent action.

## Receive safely

Use the existing [webhook verifier](https://developers.kismet.travel/guides/webhook-verification/) on the exact raw bytes before parsing. Signature header version `v1` and event envelope version `2` are independent.

```ts
import {
  verifyKismetWebhook,
  DeveloperEventSchema,
  type DeveloperEvent,
} from '@kismet-tech/sdk/server';

export async function handleKismetWebhook(
  request: Request,
  config: {
    signingSecret: string;
    applicationId: string;
    installationId: string;
    collectionId: string;
    environment: 'TEST' | 'LIVE';
    enqueueOnce: (eventId: string, event: DeveloperEvent) => Promise<void>;
  },
) {
  // Enforce your request-size limit before buffering.
  const rawBody = new Uint8Array(await request.arrayBuffer());
  const verified = verifyKismetWebhook({
    rawBody,
    headers: request.headers,
    secret: config.signingSecret,
  });
  const event = DeveloperEventSchema.parse(
    JSON.parse(new TextDecoder().decode(rawBody)),
  );
  if (
    event.id !== verified.id ||
    event.source.application_id !== config.applicationId ||
    event.source.installation_id !== config.installationId ||
    event.collection.id !== config.collectionId ||
    event.environment !== config.environment
  ) return new Response('Event scope mismatch', { status: 400 });

  await config.enqueueOnce(verified.id, event);
  return new Response(null, { status: 204 });
}
```

Your application supplies `enqueueOnce`: use a durable unique key scoped to this destination/installation, and atomically persist the event ID with the queued work. Return 2xx only after that transaction commits. Do not use an in-memory set. Map invalid signatures/schema to a 400 response without logging bodies or secrets; transient processing failures should remain retryable.

The parser retains future event types rather than crashing the whole feed. Use `isKnownDeveloperEvent` from `@kismet-tech/sdk/server` or `@kismet-tech/sdk/contracts` before applying typed wallet-specific logic. Store or quarantine unhandled types without granting them business meaning.

## Delivery and access boundaries

Delivery is at least once, not exactly once, and ordering is not guaranteed. Retries and replay use a fresh signature timestamp over the unchanged event ID and body.

Current application/installation status and event-family grants are checked at fan-out, dispatch, read, and replay. Revoking access suppresses delivery but does not erase canonical events. Restoring grants does not automatically replay suppressed events.

- 2xx acknowledges delivery. Network errors, 429 and 5xx retry; other statuses terminate that attempt series.
- The current retry budget is six attempts. Do not treat the bounded retry schedule as a delivery SLA.
- Successful delivery rows are retained for 30 days; terminal failed rows for 90 days. Event history is separate. No guaranteed event-history retention SLA is offered in this preview.
- At most 100 destinations may exist per installation, including disabled ones. URL, scope, event types and envelope version are immutable; only `enabled` can be changed.
- Enabling requires current grants for every selected family. Disabling requires `webhooks.write`.
- There is no atomic signing-key overlap rotation operation in this API. Create and verify a replacement destination, then disable the old one; deduplicate across both. Lost reveal-once secrets cannot be recovered.
- GET reads share the installation's read allowance with other reads. Writes use independent, stricter operation limits. Respect `Retry-After`; see [rate limits and retries](https://developers.kismet.travel/guides/developer-access.md#rate-limits-and-retries).
- Legacy collection-manager destinations retain envelope v1 and are not auto-enrolled. They cannot read, modify, rotate, replay, or receive these installation-scoped events.

See the [API reference](https://developers.kismet.travel/api/) for request and response schemas. The Developer MCP recipe `developer-event-webhooks` describes the same preview contract; it does not grant access or create a subscription for you.
