# Iframe protocol

The version 2 postMessage contract for readiness and code-only authorization.

Source: https://zetapad.com/developers/docs/protocol

## Transport and trust

Use `window.parent.postMessage` with an exact trusted Zetapad origin. Install a message listener before sending `ready`. Verify `event.source === window.parent` and the exact `event.origin`, then pin the responding parent origin after the readiness handshake.

Production parents are `https://zetapad.com` and `https://www.zetapad.com`. Use a fresh request ID for each request. Untrusted or malformed messages receive no response.

Version 2 supports only `ready` and `authenticate`. Member IDs and profile fields are available through backend REST authentication, including for apps with no scopes.

## Request envelope

```json
{
  "channel": "zetapad",
  "version": 2,
  "requestId": "unique-id",
  "method": "ready",
  "params": {}
}
```

`channel` must be `zetapad`, `version` must be `2`, and `requestId` must be a nonempty string of at most 128 characters. `params` must be an object. Unknown top-level fields, method names, and extra params are rejected. The app identity and scopes come from the Zetapad registration; requests cannot override them.

See [the JSON Schema](https://zetapad.com/developers/docs/protocol.schema.json) for machine-readable message shapes.

## ready

Call with `params: {}` after the iframe loads. A successful result is:

```json
{ "launchId": "zp_launch_...", "appId": "zp_app_..." }
```

Keep the launch ID for subsequent calls. It belongs to the currently mounted room; it is not an app login token. Launches expire after one hour, and authentication may extend the stored launch lifetime. Reopen the room when a launch is stale or expired.

Zetapad shows retry controls after 15 seconds without a valid readiness handshake. Install the listener and send `ready` promptly.

## authenticate

Call after `ready` with the current launch ID and a backend-generated state challenge:

```json
{ "launchId": "zp_launch_...", "state": "random-session-bound-challenge" }
```

`launchId` is a nonempty string of at most 128 characters and must match the current launch. `state` must be a nonempty string of at most 256 characters. A successful result is:

```json
{
  "code": "zp_code_...",
  "expiresIn": 60,
  "state": "random-session-bound-challenge"
}
```

The member grants every registered scope or cancels. Saved consent is reused when it remains valid. Wait for the pending consent or authentication request rather than starting another one. On cancellation, the member receives `CONSENT_DENIED` and Zetapad closes the room.

The result contains only `code`, `expiresIn`, and `state`. A successful reply means the member authorized a code; your app is not logged in until its backend completes verification.

Your backend must validate and consume the echoed state against the same visitor session before exchanging the code. The code expires after 60 seconds and can be exchanged once by its registered app using its client ID and secret. REST userinfo then returns the shared server `userId`, granted scopes, and approved fields. This flow is required even with zero scopes. See [authentication](https://zetapad.com/developers/docs/authentication).

## Response envelope

Success replies contain exactly one `result`; failure replies contain exactly one `error`. Both echo `channel`, `version`, and `requestId`:

```json
{
  "channel": "zetapad",
  "version": 2,
  "requestId": "unique-id",
  "error": { "code": "STALE_LAUNCH", "message": "Complete the ready handshake for this room first." }
}
```

Correlate each reply with its pending request ID and ignore replies for unknown requests. Handle errors by their stable `code`; messages explain the issue to the developer. See [errors and troubleshooting](https://zetapad.com/developers/docs/troubleshooting) for recovery actions.
