Metadata-Version: 2.5
Name: synorb
Version: 0.1.1
Summary: Official Python client for the Synorb REST API: source-grounded, cited context for AI agents.
Project-URL: Homepage, https://synorb.com
Project-URL: Documentation, https://synorb.com/docs
Project-URL: Release notes, https://synorb.com/sdk
Project-URL: Authentication, https://synorb.com/auth
Project-URL: OpenAPI specification, https://synorb.com/openapi.json
Author-email: Synorb <team@synorb.com>
License: MIT
Keywords: agents,api,context,mcp,oauth,rag,synorb
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# synorb

Official Python client for the [Synorb](https://synorb.com) REST API — source-grounded, cited context for AI agents.

```bash
pip install synorb
```

**Zero dependencies.** Everything is standard library. An agent installs this into an environment it does not control, so a client that cannot drag in a transitive supply chain or fight an existing `requests`/`httpx` pin is worth more than a few convenience methods.

## Quick start

```python
from synorb import SynorbClient, connect

# Free 24-hour demo key — no email, no card, no sales contact.
key = connect()["credentials"]["synorb_key"]

client = SynorbClient(token=key)
client.account()
client.streams(page_size=5)
client.stream_search("central bank policy")
```

Or read the credential from the environment:

```bash
export SYNORB_TOKEN="synorb_..."
```

```python
SynorbClient().account()
```

## Two credentials, one header

| | Synorb Key | OAuth 2.0 access token |
|---|---|---|
| Lifetime | Long-lived | Short-lived |
| Scopes | None — full plan access | Scoped, and enforced per endpoint |
| Revocable | Regenerate the key | Yes, RFC 7009 |
| Get one | [synorb.com/connect](https://synorb.com/connect) | [`synorb.oauth`](#oauth-20) |

Both go in `Authorization: Bearer`. The client does not try to tell them apart — the server does. The Synorb Key is the default and is **not** deprecated; most integrations should just use it.

## OAuth 2.0

Authorization code + PKCE (`S256` only). Public clients, no client secret, and **no dynamic client registration** — `client_id` values are issued by Synorb.

```python
from synorb import SynorbClient, oauth

pkce = oauth.generate_pkce()
state = oauth.generate_state()

url = oauth.build_authorize_url(
    client_id="synorb-cli",
    redirect_uri="http://127.0.0.1:8765/callback",
    code_challenge=pkce.challenge,
    scope="read:api read:mcp",     # ask for only what you need
    state=state,
)
# open `url`, sign in, catch the redirect on your loopback listener

tokens = oauth.exchange_code(
    code=code_from_redirect,
    redirect_uri="http://127.0.0.1:8765/callback",
    client_id="synorb-cli",
    code_verifier=pkce.verifier,
)
client = SynorbClient(token=tokens["access_token"])
```

Two things to know before building against it:

- **`scope` is honoured exactly.** Omit it and you get the full default read-only grant. Ask for something that cannot be granted — a `write:` scope, or anything outside that default — and the request is refused with `invalid_scope` rather than quietly trimmed, so a token never claims a permission it does not hold.
- **Refresh tokens rotate, and reuse is treated as theft.** Presenting a refresh token twice revokes the entire family. Store the newest one you were handed; never retry with an old one.

Discovery, if you would rather read it off the wire:

```python
oauth.discover()   # https://api.synorb.com/.well-known/oauth-authorization-server
```

## Errors you can actually act on

Synorb's error envelopes are actionable, so they are not collapsed into a bare `HTTPError`:

```python
from synorb import SynorbAuthError, SynorbInsufficientScope, SynorbRateLimited

try:
    client.account()
except SynorbInsufficientScope as exc:
    exc.required_scope        # "read:webhooks" — the scope the token lacked
except SynorbRateLimited as exc:
    exc.retry_after           # seconds, from Retry-After
except SynorbAuthError as exc:
    exc.resource_metadata     # RFC 9728 pointer to the authorization server
```

`SynorbInsufficientScope` can only come from an OAuth token. A Synorb Key has no scope concept and is never scope-checked.

## Quota

`manifests_query()` bills only the Manifests it returns. `mode="count"`, `"orient"` and `"plan"` are zero-quota previews — plan first, then replay the exact body with the returned `plan_token`:

```python
plan = client.manifests_query(mode="plan", stream_ids=["..."], lookback_hours=24)
rows = client.manifests_query(stream_ids=["..."], lookback_hours=24, plan_token=plan["data"]["plan_token"])
```

Discovery calls (`streams`, `stream_search`, `ask`, `ontology_tags`) do not consume Manifest quota.

## No billing methods, on purpose

The [`@synorb/cli`](https://www.npmjs.com/package/@synorb/cli) package has `synorb billing checkout`, which opens a hosted Stripe page for a human to complete. That is a sensible thing to type and a bad thing for a library to expose — an agent calling `client.checkout()` in a retry loop is a failure mode worth designing out rather than documenting around. Billing stays a deliberate, human, command-line act.

## Links

- Docs — <https://synorb.com/docs>
- Authentication — <https://synorb.com/auth>
- OpenAPI — <https://synorb.com/openapi.json>
- MCP server — <https://synorb.com/docs/mcp>
- CLI — <https://www.npmjs.com/package/@synorb/cli>

## Security

Keep the credential in a server-side secret store. It never belongs in browser code, a repository, logs, or a prompt. Report issues to team@synorb.com.

MIT licensed.
