# Connect Fryri to a booking app

> Use a shared policy, look up a member’s bookings and ask for approval before booking a class.

Source: https://fryri.com/docs/api/put-fryri-behind-your-app

Fryri runs the agent in the cloud. In this guide, a gym's app sends a member's request and handles the booking. If you haven't made an API call yet, start with the [quickstart](/docs/api#quickstart).

## The example

A gym member opens Thursday's spin class and asks, “When did I last miss a class? Can I bring a guest to this one? Book us in if you can.” The app already has the class ID from the page the member is viewing.

The agent needs three things to answer:

- The guest policy, which is the same for every member. It goes in the shared library.
- The member's bookings, which live in the gym's own service. The app looks them up through an action.
- The member's OK before booking. The booking action asks for approval first.

Every call below runs on your server with your key in `FRYRI_API_KEY`. The member is `member_812`, the id the gym's own app already uses for them.

## Add what every member reads

Use `scope: "shared"` to make the guest policy available to every member's run.

**Request**

```bash
curl -X POST https://api.fryri.com/v1/memories \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Members may bring one guest to a class if places are available. Guests sign the waiver at the front desk before the class starts.",
    "title": "Guest policy",
    "source_ref": "policies/guests.md",
    "scope": "shared"
  }'
```

Wait for the memory to report `ready` before starting the run. To update the policy later, add the new text with the same `source_ref`.

## Describe your actions

`PUT /v1/actions` tells the agent what your app can do. Each action has a `name`, a plain-words `description` the agent reads, an `input` JSON Schema, and `needs_approval`. The list replaces the previous one as a whole.

**Request**

```bash
curl -X PUT https://api.fryri.com/v1/actions \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "actions": [
      {
        "name": "find_bookings",
        "description": "List the member'\''s class bookings and attendance between two dates.",
        "input": {
          "type": "object",
          "properties": {
            "from": {"type": "string", "format": "date"},
            "to": {"type": "string", "format": "date"}
          },
          "required": ["from"]
        }
      },
      {
        "name": "book_class",
        "description": "Book the member and any guest into the class if places are available.",
        "input": {
          "type": "object",
          "properties": {
            "class_id": {"type": "string"},
            "guests": {"type": "integer", "minimum": 0, "maximum": 1}
          },
          "required": ["class_id"]
        },
        "needs_approval": true
      }
    ]
  }'
```

`find_bookings` only reads, so it runs straight away. `book_class` changes something for the member, so it waits for their approval.

## Start a run

`POST /v1/runs` starts the agent on the member's message. `end_user_id` makes it the member's run: it reads their library, and its thread belongs to them.

**Request**

```bash
curl -X POST https://api.fryri.com/v1/runs \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "When did I last miss a class? Can I bring a guest to this one? Book us in if you can. Selected class: spin_thu_1800.",
    "end_user_id": "member_812"
  }'
```

**Response**

```json
{
  "id": "run_5f0c2a9e41d87b3c6e20f914",
  "thread_id": "thr_8a31d07c5be24f96a1e0c3d2",
  "end_user_id": "member_812",
  "status": "needs_app",
  "reply": null,
  "sources": [],
  "files": [],
  "error": null,
  "cost_usd": null,
  "created_at": "2026-11-07T09:14:02.118407Z",
  "finished_at": null
}
```

The call waits up to 55 seconds. `202` with `status: "running"` means Fryri is still working. `202` with `status: "needs_app"` means it is waiting for your app. In either case, follow the run's events.

## Follow the run

`GET /v1/runs/{run_id}/events` streams what the run does as server-sent events. The run id is enough: it belongs to your account, so you don't need to pass `end_user_id` to read, follow, stop or answer the run. If you do pass it, it must be the run's own, or the call returns `404`.

**Request**

```bash
curl -N "https://api.fryri.com/v1/runs/run_5f0c2a9e41d87b3c6e20f914/events" \
  -H "Authorization: Bearer $FRYRI_API_KEY"
```

**Response**

```text
id: 1
data: {"seq":1,"type":"progress","step":"step_1","status":"started","label":"Searching the library"}

id: 2
data: {"seq":2,"type":"progress","step":"step_1","status":"finished","label":"Searching the library","outcome":"succeeded","targets":[{"kind":"document","label":"Guest policy"}]}

id: 3
data: {"seq":3,"type":"action_request","request_id":"req_91b0e4d27c3a58f6b2d0e817","expires_at":"2026-11-07T09:29:05.664120Z","action":"find_bookings","input":{"from":"2026-10-01"}}
```

If the connection drops, reconnect with `after` set to the last `seq` you received. The Python and TypeScript clients handle this when following run events.

## Run the action

An `action_request` asks your app to run one of its actions. Run it in your own code, then send what it returned to `POST /v1/runs/{run_id}/respond` with the event's `request_id`.

**Request**

```bash
curl -X POST https://api.fryri.com/v1/runs/run_5f0c2a9e41d87b3c6e20f914/respond \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "request_id": "req_91b0e4d27c3a58f6b2d0e817",
    "result": {
      "bookings": [
        {"class": "Yoga, Tuesday 7am", "date": "2026-10-27", "attended": false},
        {"class": "Spin, Thursday 6pm", "date": "2026-10-29", "attended": true}
      ]
    }
  }'
```

`result` accepts up to 256 KB of JSON. If the action fails, send an `error` with the reason so Fryri can explain what happened.

## Ask the member

The run continues and wants to book. Because `book_class` has `needs_approval`, the next event is an `approval_request` instead of an `action_request`:

```text
id: 4
data: {"seq":4,"type":"approval_request","request_id":"req_2c7e91f04ab35d68e0f1a9c4","expires_at":"2026-11-08T09:14:19.250981Z","action":"book_class","input":{"class_id":"spin_thu_1800","guests":1},"description":"Book the member and any guest into the class if places are available."}
```

Show the member your own button with what the action will do. When they tap it, send their decision:

**Request**

```bash
curl -X POST https://api.fryri.com/v1/runs/run_5f0c2a9e41d87b3c6e20f914/respond \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_id": "req_2c7e91f04ab35d68e0f1a9c4", "decision": "approve"}'
```

After `approve`, wait for the new `action_request` before booking. Use that event's `request_id` to send the booking result, just as you did for `find_bookings`. If the member chooses `decline`, Fryri continues without booking. Approval requests expire after 24 hours. Action requests expire after 15 minutes.

## Read the reply

The run settles with a `done` event that carries the run, and `GET /v1/runs/{run_id}` returns the same object.

**Response**

```json
{
  "id": "run_5f0c2a9e41d87b3c6e20f914",
  "thread_id": "thr_8a31d07c5be24f96a1e0c3d2",
  "end_user_id": "member_812",
  "status": "done",
  "reply": "You last missed the 7am yoga class on Tuesday 27 October. The gym allows one guest if there’s room [1], and I’ve booked both of you into Thursday’s 6pm spin.",
  "sources": [
    {
      "type": "document",
      "id": "file_Qm81vTzKc0aR2wLb",
      "title": "Guest policy",
      "text": "Members may bring one guest to a class if places are available. Guests sign the waiver at the front desk...",
      "source_ref": "policies/guests.md",
      "citation": 1,
      "cited": true,
      "library": "shared"
    }
  ],
  "files": [],
  "error": null,
  "cost_usd": 0.016558,
  "created_at": "2026-11-07T09:14:02.118407Z",
  "finished_at": "2026-11-07T09:15:41.902733Z"
}
```

Pass the returned `thread_id` with the member's next message to continue the conversation.

## Hear about runs with a webhook

A person can take hours to approve. A webhook lets your server stop holding a stream open: Fryri sends a POST when a run needs your app or settles.

**Request**

```bash
curl -X POST https://api.fryri.com/v1/webhooks \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://gym.example.com/fryri/webhook"}'
```

The response carries a `secret` that starts with `whsec_`. It is shown only once, so store it as `FRYRI_WEBHOOK_SECRET`.

A delivery names the run and the open request, never the reply or what anyone wrote:

```json
{
  "id": "evt_5821",
  "type": "run.needs_app",
  "created_at": "2026-11-07T09:14:19.251377Z",
  "data": {
    "run_id": "run_5f0c2a9e41d87b3c6e20f914",
    "thread_id": "thr_8a31d07c5be24f96a1e0c3d2",
    "end_user_id": "member_812",
    "status": "needs_app",
    "request": {"id": "req_2c7e91f04ab35d68e0f1a9c4", "kind": "approval", "action": "book_class"}
  }
}
```

The `Fryri-Signature` header reads `t={unix seconds},v1={hex}`. `v1` is the HMAC-SHA256 of `{t}.{raw body}` keyed with the secret. Verify it on the raw bytes before parsing the body, and answer with a `2xx` within 5 seconds.

### Verify and handle deliveries

Pass the raw HTTP body and the signature header to the SDK before using anything in the payload.

Python, with the [Python client](https://fryri.com/sdk/fryri.py):

```python
from fryri import verify_webhook

event = verify_webhook(raw_body, signature_header, webhook_secret)
```

TypeScript, with the [TypeScript client](https://fryri.com/sdk/fryri.ts):

```typescript
import { verifyWebhook } from "./fryri.ts";

const event = await verifyWebhook(rawBody, signatureHeader, webhookSecret);
```

Both clients raise a `WebhookSignatureError` for an invalid signature. After verifying a delivery, queue it and return a `2xx` response. Use the event's `id` to recognise repeated deliveries. For a pending action, read its input from the run's event stream and use its `request_id` to make sure your service only carries out the action once. Then post the result to the run. For an approval request, show the choice in your app and wait for the member to answer.

## Prices

You pay for what the model and tools use, the same as the Fryri app. A run reports what it charged in `cost_usd`, and web searches and page reads have their own listed prices. Actions, approvals, events and webhooks are free. See [Pricing](/docs/api#pricing).
