Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
project adheres to [Semantic Versioning](https://semver.org/).

## [0.5.0rc2] - 2026-10-19

Internal release candidate for 0.5.0.

### Added

- Pine Platform API support for partners (businesses that build on Pine).
`AsyncPineAI` and `PineAI` accept a partner's secret key as `api_key`.
`client.platform.managed_users.create()` and `.get()` take your `external_id`,
return typed `ManagedUser` values, and raise `PlatformError` without upstream
bodies.
- Managed-user mode: `api_key` plus `user_id`, where `user_id` is the
`ManagedUser.id` (the Pine user ID). REST requests send the key and a
`Pine-Managed-User-Id` header. The Socket.IO handshake sends
`{"token", "managed_user_id"}`, and envelopes carry the same ID as
`user_id`. `connect()` sends no REST request first. Injected HTTP clients
cannot add or override the header.
- In key mode, `user_id` must be a string in canonical decimal form: 1 to
9223372036854775807, with no sign, spaces or leading zeros. An `int` is
refused. `validate_user_id` exposes this check.
- A client with `api_key` and no `user_id` can call only the Platform API. A
user-scoped call from it raises `ValueError` before any request is sent.
These also raise `ValueError`: `api_key` with `access_token`, identity
overrides passed to `connect()` in key mode, `platform.*` on a client without
`api_key`, and `auth.verify_code()` or `set_token()` on a client with
`api_key` (checked before the code is sent). A Platform key passed as
`access_token` raises `ValueError` that says to pass it as `api_key`.
- `API_KEY_PREFIXES` (`pine_sk_live_`, `pine_sk_test_`), `validate_external_id`,
`validate_user_id` and `MANAGED_USER_HEADER` are public. The
managed-user header is sent only with the API key it was configured with, and
identity headers are applied after caller headers, so neither can be
overridden per request. Error messages never contain the key, the
`external_id` or the user ID.
- `sessions.history()` on the async and sync clients reads one page of a
session's stored messages over REST (`GET /v2/sessions/{id}/messages`), so
a client without Socket.IO can read the agent's replies. It returns
`SessionHistoryPage` and raises `SessionError`.
- Docs for managed users: `docs/sdk-api.md` (SDK methods), `docs/http-api.md`
(raw HTTP API) and `docs/socketio.md` (Socket.IO, reference only).
- In key mode, a `connect()` that gets no `ready` event within `ready_timeout`
raises `ConnectionError` with the hint to check `api_key` and `user_id`. The
Pine backend refuses a bad handshake by closing the connection, with no
`connect_error`.

## [0.5.0rc1] - 2026-09-18

Internal release candidate for 0.5.0. See [MIGRATION.md](MIGRATION.md) before
Expand Down
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,104 @@ never retry or replay form submissions. A transport ACK is not a delivery
receipt. The legacy synchronous `send_form_response()` is deprecated because it
cannot verify the original form request.

## Platform API for partners

A *partner* is a business that builds on Pine through the Platform API. It
holds a secret key, `pine_sk_live_...`. Keep it on your servers: read it
from an environment variable or secret store, and never ship it in browser,
mobile or other client-side code, prompts, or logs. To get a key, contact
support@19pine.ai. Each end user of your product becomes a *managed user*,
identified by your own `external_id` (1-128 of letters, digits, `.`, `_`, `:`,
`|`, `-`, not starting with `.`; no personal data).

All managed users of one partner use that partner's credits. A partner can
limit the total credits that its managed users use.

The SDK methods for managed users are listed in
[docs/sdk-api.md](docs/sdk-api.md). If you do not use Python, see the HTTP API
in [docs/http-api.md](docs/http-api.md) and the Socket.IO reference in
[docs/socketio.md](docs/socketio.md).

Create the managed user once, and store its `id` in your own user table. The
call is idempotent per `external_id`: a repeat returns the stored user
unchanged, even if the profile you send differs. `phone` is optional, in E.164
form, and must be a number you have verified. `get()` finds a user by
`external_id` if you did not store the `id`.

```python
import os

from pine_assistant import AsyncPineAI

api_key = os.environ["PINE_API_KEY"]

async with AsyncPineAI(api_key=api_key) as partner_client:
user = await partner_client.platform.managed_users.create(
"usr_01J9Z3K8QF",
email="ada@example.com",
name="Ada Lovelace",
phone="+14155550100",
)
same_user = await partner_client.platform.managed_users.get("usr_01J9Z3K8QF")
# Save user.id with your own user record.
```

`user.id` is the Pine user ID, a decimal string such as `"123456789"`. To act
as that user, pass it as `user_id` together with the key. Every user-scoped
REST request then sends the key and a `Pine-Managed-User-Id` header. The
Socket.IO handshake sends the key and `managed_user_id`, and every Socket.IO
message carries the same ID. The SDK checks the form of `user_id` when you
create the client. It accepts only a string, never an `int`.

```python
async with AsyncPineAI(api_key=api_key, user_id=user.id) as client:
session = await client.sessions.create()
await client.connect()
await client.join_session(session["id"])
message = "Call my dentist to move Tuesday's appointment"
async for event in client.chat(session["id"], message):
print(event.type, event.data)
```

A client with `api_key` and no `user_id` can call only the Platform API. A
user-scoped call from it, such as `sessions.list()` or `connect()`, raises
`ValueError` before any request is sent. These also raise `ValueError`:
`api_key` together with `access_token`, `platform.*` on a client without
`api_key`, and `auth.verify_code()` or `set_token()` on a client with `api_key`.
A key client keeps its key; use a separate client for a user token.

Every error below is a `PineAIError` with `code` and `status_code`, so catch
`PineAIError`. Some methods raise a subclass: `SessionError` from
`sessions.list`, `get`, `send_message`, `history` and `outcomes`, and
`AuthError` from `auth.*`. Other `sessions` methods, such as `create` and
`start_task`, raise `PineAIError` itself.

- 400 `invalid_request`: the backend did not accept the managed-user ID.
- 404 `platform_managed_user_not_found`: the ID is not one of your managed users.
- 403 `platform_managed_user_unavailable`: the managed user is banned or
archived.
- `client.auth.mint_ticket()` always fails with 403
`platform_route_not_allowed`, raised as `AuthError`. Managed users cannot mint
login tickets. Other routes closed to a partner key fail in the same way.

Billing and admission can also reject sessions and chat for a managed user:
402 `platform_spend_limit_reached`, 403 `platform_access_suspended`, 409
`platform_session_state_conflict`, 409 `platform_session_limit_reached` from
`sessions.start_task` when the running-task limit is reached.

If the server refuses the Socket.IO handshake, it closes the connection and
sends no error code. `connect()` then raises `ConnectionError` when
`ready_timeout` (15 seconds by default) ends. Its `code` is always
`connection_error`, so it does not tell you the cause. The message tells you to
check `api_key` and `user_id`.

`PineAI` accepts the same `api_key` and `user_id` for synchronous REST. Platform
API failures raise `PlatformError` with a stable `code` and `status_code` (for
example 401 for a bad key, 403 for a suspended partner, 404
`platform_managed_user_not_found`, 409 `platform_managed_user_limit_reached`).
Like every SDK error, it never reproduces the upstream body, the key, the
`external_id` or the user ID.

## Quick Start (Sync REST)

`PineAI` is a synchronous REST client. It returns values directly for auth and
Expand Down
154 changes: 154 additions & 0 deletions docs/http-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# HTTP API for managed users

This page describes the raw HTTP API that a partner (a business that builds on
Pine) uses to act for its managed users.

## Get a key

To get a Platform API secret key, contact support@19pine.ai.

Keep the key on your servers. Do not put it in browser or mobile code, in
prompts, or in logs.

## Managed users

All managed users of one partner use that partner's credits. A partner can
limit the total credits that its managed users use.

Each end user of your product is a *managed user*. You name the user with your
own `external_id` when you create it. Pine gives it a Pine user ID, `id`. Store
`id` in your own user table: you need it for every request that acts for the
user.

### Response format

All paths start with `https://www.19pine.ai/api`.

- Success: `{"status": "success", "data": ...}`
- Error: `{"status": "error", "error": {"code": "...", "message": "..."}}`

Use `error.code` in your code. The `message` text can change.

### Create a managed user

`POST /platform/v1/managed-users` with `Authorization: Bearer <key>`.

| Field | Required | Rule |
|---|---|---|
| `external_id` | yes | 1-128 letters, digits, `.`, `_`, `:`, `|`, `-`; must not start with `.`; no personal data |
| `email` | yes | email address |
| `name` | yes | 1-100 characters |
| `phone` | no | E.164, for example `+14155550100`; a number you have verified |

> **Caution:** use only the end user's own email address and phone number, and
> verify them first. Never use an email address or phone number that does not
> belong to that user. Pine trusts these values and does tasks with them: for
> example, the agent can send email for the user. If Pine finds misuse, Pine
> can revoke the key and cancel the partner's access.

The call is idempotent per `external_id`. The first call returns 201. A
repeated call returns 200 with the stored user unchanged, even if the fields
you send are different.

The response `data` is the managed user:
`{"id", "external_id", "email", "name", "phone", "created_at"}`. `phone` is
`null` when you did not give one.

Errors: 400 `invalid_request`, 401 `unauthorized`, 403
`platform_access_suspended`, 409 `platform_managed_user_limit_reached`.

### Get a managed user

`GET /platform/v1/managed-users/{external_id}` with `Authorization: Bearer <key>`.
Percent-encode `external_id` in the path (for example `|` is `%7C`).

It returns the same managed user as create. A user that does not exist, or that
belongs to another partner, returns 404 `platform_managed_user_not_found`.

### Act for a managed user

Send both headers on a `/v2` request:

```
Authorization: Bearer <key>
Pine-Managed-User-Id: <id>
```

`<id>` is the `id` from create or get, as a decimal string: 1 to
9223372036854775807, with no sign, spaces or leading zeros. Do not send
`Pine-Managed-User-Id` on `/platform/v1` requests.

| Status | Code | Cause |
|---|---|---|
| 400 | `invalid_request` | One of the two headers is missing, the header is sent more than once, or the ID is not in the form above. |
| 401 | `unauthorized` | The key is wrong or revoked. |
| 403 | `platform_access_suspended` | The partner is suspended. |
| 403 | `platform_managed_user_unavailable` | The managed user is banned or archived. |
| 403 | `platform_route_not_allowed` | The route is closed to keys: login tickets, phone verification, login linking, push registration, purchases and subscriptions, and computer settings. |
| 404 | `platform_managed_user_not_found` | The ID is not a managed user of this partner. |

### Session routes

These routes take the two headers above.

| Request | What it does | SDK |
|---|---|---|
| `GET /v2/auth/me` | Returns `{"user_id": "<id>"}`. | `auth.me()` |
| `POST /v2/sessions` | Creates a session. | `sessions.create()` |
| `GET /v2/sessions` | Lists the user's sessions. | `sessions.list()` |
| `GET /v2/sessions/{id}` | Returns one session. | `sessions.get()` |
| `POST /v2/sessions/{id}/messages` | Sends a message. Body: `{"content": "..."}`. | `sessions.send_message()` |
| `GET /v2/sessions/{id}/messages` | Reads the stored messages, newest first. | `sessions.history()` |
| `POST /v2/sessions/{id}/start` | Starts a prepared or paused task. | `sessions.start_task()` |
| `POST /v2/sessions/{id}/stop` | Stops a running task. | `sessions.stop_task()` |
| `GET /v2/sessions/{id}/outcomes` | Lists the session's outcomes. | `sessions.outcomes()` |

Routes that make the agent work can also return these errors:

| Status | Code | Cause |
|---|---|---|
| 402 | `platform_spend_limit_reached` | The partner's credit limit is reached. |
| 409 | `platform_session_limit_reached` | Start: the partner's running-task limit is reached. |
| 409 | `platform_session_state_conflict` | Message: the session is finished or cancelled. |

`GET /v2/sessions/{id}/messages` takes two optional query values: `limit`
(1-100, default 30) and `before_message_id`. Each message has `message_id`,
`type`, `version`, `data` (a JSON string) and `created_at`. To read older
messages, send the returned `next_message_id` as `before_message_id`. An empty
`next_message_id` means there are no older messages.
A new reply can take a few seconds to appear in this list. If the `state`
changed but the reply is missing, read again.

Other `/v2` routes accept the key and header in the same way. This page does
not describe them.

### How a task runs

1. Create a session and send the user's request as a message.
2. The agent talks with the user to get the information that it needs.
3. The agent prepares the task. The task then starts on its own, as it does for
a normal Pine user.
4. The agent does the task. It can ask the user more questions.
5. The task ends.

The session's `state` shows where the task is:

| `state` | Meaning |
|---|---|
| `init` | The session is new. |
| `chat` | The agent talks with the user. |
| `task_ready` | The task is prepared. It usually starts on its own. If the running-task limit is reached, it stays here: call `start` later. |
| `task_processing` | The task runs. |
| `task_paused` | The task is paused, for example because the credit limit is reached. When the cause is removed, call `start` to continue. |
| `task_finished` | The task is done. Read the result with `outcomes`. |
| `task_cancelled` | The task was cancelled. |

### Talk with the agent

We recommend Socket.IO ([docs/socketio.md](socketio.md)) to talk with the
agent. You get its replies, its questions and each change of `state` as they
happen.

Over HTTP only, send messages with `POST /v2/sessions/{id}/messages`. Read the
replies and the `state` with `GET /v2/sessions/{id}/messages` and
`GET /v2/sessions/{id}`.
Loading
Loading