
# Hosted link events

A hosted verification page (`https://trust.myaza.co/verify/…`) tells whatever is hosting it what the applicant is doing: when the flow is ready, which step they are on, when they submit, and when something goes wrong. Nothing to install. Open the link in a WebView (or an iframe) and listen.

The events are deliberately light. They carry ids, step names and error codes, never anything the applicant typed or captured. The result itself still arrives by [webhook](https://trust.myaza.co/documentation/webhooks/markdown) and [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown), exactly as before.

## Which link to open

Mint a **per-applicant session** from your backend with [`POST /api/kyc/sessions`](https://trust.myaza.co/documentation/api-create-session/markdown) and open its `url`. That link belongs to one person and carries your `externalUserId`, so every event you receive is already about a user you know. The `sessionId` it returns is also the verification's id once the applicant submits.

A shared hosted link (`/verify/workflow/<linkToken>`) emits the same events, but a shared link is one URL for many people, so the events cannot tell you who is verifying.

## The events

Every message is a JSON object with `source: "myaza-kyc"` and a `type`:

| `type` | When | Fields |
|---|---|---|
| `ready` | The page loaded the session and is about to show the first step. | `sessionId`, `environment`, `subjectType`, `scope` |
| `started` | The flow mounted. | `sessionId` |
| `step` | The applicant reached a step, including the one the flow opened on. | `sessionId`, `step` |
| `submitted` | The applicant submitted. Processing continues on the server. | `sessionId`, `verificationId`, `status` (`pending`) |
| `completed` | A returning applicant opened a link whose verification was already submitted. | `sessionId` |
| `error` | A technical error (network, camera permission, expired link). | `sessionId`, `code`, `message` |
| `closed` | An embedded flow's Done or close was pressed. | `sessionId` |

`step` values are the SDK's step names: `consent`, `email-verification`, `phone-verification`, `country-select`, `id-type`, `id-input`, `document-capture`, `liveness`, `proof-of-address`, `address-search`, `address-collection`, `address-entrance`, `address-review`, `questionnaire`, `business-details`, `business-key-people`, `business-documents`, `applicant-role`, `submitted`. `error` codes are the SDK's `KYCError` codes (`network_error`, `camera_permission_denied`, `upload_failed`, `feature_disabled`, `insufficient_credits`, `invalid_api_key`, `unknown`).

New event types may be added; existing ones are never renamed. Ignore types you do not recognise.

## React Native (`react-native-webview`)

The page posts to the WebView's own channel, so there is nothing to configure on the page side:

```tsx
import { WebView } from 'react-native-webview';

<WebView
  source={{ uri: session.url }}
  allowsInlineMediaPlayback
  mediaPlaybackRequiresUserAction={false}
  onMessage={(event) => {
    const message = JSON.parse(event.nativeEvent.data);
    if (message.source !== 'myaza-kyc') return;
    if (message.type === 'submitted') {
      // message.verificationId is the id to poll or match against your webhook.
      navigation.replace('VerificationPending', { id: message.verificationId });
    }
    if (message.type === 'error') console.warn('KYC error', message.code);
  }}
/>
```

The camera works inside `react-native-webview` on iOS 14.3+ and on Android once your app grants the WebView's camera permission request (`onPermissionRequest`). NFC chip reading, background presence checks and native liveness are not available inside a WebView; if your workflow needs them, use the [React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown).

## Flutter (`webview_flutter`)

Register a JavaScript channel named `MyazaKYC`; the page posts to it when it exists:

```dart
final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel('MyazaKYC', onMessageReceived: (message) {
    final event = jsonDecode(message.message) as Map<String, dynamic>;
    if (event['source'] != 'myaza-kyc') return;
    if (event['type'] == 'submitted') {
      // event['verificationId']
    }
  })
  ..loadRequest(Uri.parse(session.url));
```

## In an iframe

Append your page's origin to the link as `origin`, and the page posts to that window only when the browser confirms it really is the embedder:

```html
<iframe src="https://trust.myaza.co/verify/<token>?origin=https://app.example.com" allow="camera; microphone; geolocation"></iframe>
```

```js
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trust.myaza.co') return;
  if (event.data?.source !== 'myaza-kyc') return;
  // event.data.type, event.data.step, event.data.verificationId …
});
```

An `origin` that does not match the page's actual parent is ignored: no events are posted at all, rather than to a window that should not read them.

## What the events are not

They are a courtesy to the host's UI, not the record. Treat `submitted` as "the applicant is done with the flow"; the verdict still comes from your [webhook](https://trust.myaza.co/documentation/webhooks/markdown) or from polling [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) with the `sessionId` you minted.
