Skip to content

Fryri documentation

Connect Fryri to a booking app

Updated 2026-11-11

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.

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

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

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

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

{
  "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

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

Response

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

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:

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

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

{
  "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

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:

{
  "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:

from fryri import verify_webhook

event = verify_webhook(raw_body, signature_header, webhook_secret)

TypeScript, with the TypeScript client:

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.