# Synorb agent guide: errors, deadlines, and failure contracts

This page is part of the Synorb agent guide at https://synorb.com/agent-guide.md.
Deadlines, sharding of large scopes, preflight failures and retries, the full body of each authentication and provisioning error, the failure contracts table, and the pagination contract table.

## Non-negotiable rules, continued: deadlines, sharding, preflight failures, and retries

REST and MCP have different Manifest deadlines. REST `POST /manifests/query`,
`/manifests/plan`, and `/manifests/exec` accept `timeout_ms`. Its default is
5,000 ms, clamped to 100-15,000 ms. MCP `synorb-manifests` has no `timeout_ms`
argument. Its fixed interactive budget defaults to 12 seconds. Synorb clamps
its deployment setting to 8-18 seconds.

For scopes above roughly 200-250 Streams, use disjoint shards of about 200
Streams. Make one request/tool call per shard. Then merge the results and
deduplicate by `manifest_id`. A smaller `page_size` does not avoid evaluation
of the full scope when an exact count is required. A higher `timeout_ms` does
not reduce full-scope work either. On REST, use the same disjoint sharding and
send a separate request per shard, each with its own timeout budget.

For an authorized `stream_ids` + `days` scope, `mode:"count"` and
`mode:"orient"` return an exact integer `total_count`. They do not bill Manifest
usage allowance. If the server cannot prove the total, the response is a failed
preflight with `error_code:"exact_count_unavailable"`. This does not mean
missing caller scope. Retry later, or use `mode:"default"` for a content page
(normal usage rules apply).

`final_scope_authorization_failed` is a different failure. Resolve the access or
scope problem before you retry.

A transient index-consistency failure returns
`error_code:"index_proof_unavailable"`, `retryable:true`, and
`action:"retry_same_request"`. Re-POST the same body to `action_url` after a
short delay. Send exec retries to `/manifests/exec` again. The server makes at
most one internal read retry per strategy attempt within the original deadline.
Failed attempts return no Manifests and consume no Manifest usage allowance.
Synorb does not automatically retry unknown scope-proof failures and genuine
access denials.

If no exact `tag_names` resolve, the call returns `tag_names_unresolved` with
`unresolved_tag_names` and retry guidance. If some names resolve,
`tag_logic=or` can run on the resolved scope alone. `tag_logic=and` does not
run while any requested Tag name is unresolved. It never drops an unknown
required Tag to broaden the query.

`POST /manifests/plan` rejects a `timeout_ms` value outside 100-15,000 ms with
`invalid_request`. Query and exec clamp the value into that range instead.

## Actionable authentication and provisioning errors

Every error below is a top-level JSON object. Agents should route on
`error_code` and use the `action_url`. Do not parse the human-readable prose.
The shared envelope is `error`, `error_code`, `action`, and `action_url`, with
recovery fields for each state.

<!-- SYNORB:FACTS:errors:START -->
A 401, a 404 and a demo-key 429 return a top-level object with `error`, `error_code`, `action` and `action_url`. Read `error_code` first. Other errors return `{"detail": ...}`. The `detail` value is a string, or an object that holds `error_code`.

| Status | `error_code` | What to do |
|--------|--------------|------------|
| 401 | `synorb_key_invalid` | The key is wrong or unknown. Mint a new demo key. Do not retry the same key. |
| 401 | `synorb_key_expired` | The demo key has expired. Mint a new demo key at /connect, or sign up. |
| 401 | `oauth_token_invalid` | The OAuth token was refused. Get a new token. |
| 403 | none | The plan does not include this feature. Read `required_plan`. |
| 403 | `content_not_yet_available` (field `error`, in `detail`) | The dates are past `available_through`. Read `available_through`, then ask for dates up to it. |
| 404 | `not_found` | Check the path, method and identifiers. `action_url` links the docs. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was used with a different body. Send a new key. |
| 422 | `beacon_limit_exceeded` | The Beacon limit is reached. Archive a Beacon or change plan. |
| 429 | `demo_key_rate_limited` | The network reached the demo-key limit. Read `next_step` and `bind_email`, then bind an email (Starter). |
| 429 | `rate_limited` (in `detail`) | Too many requests. Wait `retry_after_seconds` (or the `Retry-After` header), then retry. |
| 429 | `quota_exhausted` (in `detail`) | The allowance is used. Read `quota_resets_on` and wait for it. On a demo key, read `key_expires_at` and `starter_bind`. A used-up demo has no `Retry-After`. |
<!-- SYNORB:FACTS:errors:END -->

The sections below give the full body of each case.

### Demo-key provisioning rate limit — HTTP 429

The anonymous `/connect` endpoint uses an atomic limit on new demo keys per IP.

<!-- SYNORB:FACTS:mint-cap:START -->
One network address can get 3 new demo keys in 24 hours. A call that sends a valid `visitor_token` gets its same key back and does not count. Past the cap, the answer is HTTP 429 with `error_code: demo_key_rate_limited` and `next_step: "bind_email"`. Bind an email to the key you hold, or get a Starter key, and the limit does not apply.
<!-- SYNORB:FACTS:mint-cap:END -->

Calls that present a valid `visitor_token` mint nothing and do not
count against it. A call without one mints a new key and counts. Calling more
than one mint path does not trigger the 429 by itself.

This is an integration ramp, not a Manifest usage allowance error. It
does not reject shared user agents, so it treats curl, browsers, and headless
clients equally. Stop minting keys and bind an existing key. The same key
remains valid and becomes Starter. A bound key is identity-authenticated, and
the anonymous IP provisioning limit does not apply to it.

```json
{
  "error": "Rate limit exceeded. Max 3 new demo keys per network in 24 hours.",
  "error_code": "demo_key_rate_limited",
  "action": "bind_email",
  "action_url": "https://api.synorb.com/api/connect/bind-email",
  "next_step": "bind_email",
  "message": "This network reached the limit of 3 new demo keys in 24 hours. Reuse the key and `visitor_token` you saved. To keep a key that never expires, bind an email. The free Starter plan gives 100 Manifests a month and does not expire. Use POST https://api.synorb.com/api/connect/bind-email, the Advanced MCP tool `synorb-bind-email`, or the page https://synorb.com/connect. You can ask for a new demo key again in 24 hours.",
  "upgrade": "Bind an email to upgrade to Starter (free forever, 100 Manifests/month) - your same key keeps working: POST /api/connect/bind-email",
  "bind_email": {
    "rest": "POST https://api.synorb.com/api/connect/bind-email",
    "mcp_advanced": "synorb-bind-email",
    "page": "https://synorb.com/connect"
  },
  "signup": "https://synorb.com/signup",
  "retry_after": "24h",
  "retry_after_seconds": 86400,
  "limit": 3,
  "window_hours": 24,
  "same_key": true,
  "limit_scope": "ip_per_24h"
}
```

POST the bind action with the current key:

```bash
curl -sS -X POST https://api.synorb.com/api/connect/bind-email \
  -H "Authorization: Bearer $SYNORB_KEY" \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","name":"Optional name"}'
```

### Suspicious activity — HTTP 429

This is anomaly detection on provisioning paths for specific partners. It is
not a usage limit or a pricing tier. Public `/connect` uses the IP limiter above
and does not reject a shared user agent. Do not retry in a loop, and do not try
to route around a partner block with more anonymous keys. Legitimate builders
can use the identity option below (bind an email). If that does not apply,
contact `team@synorb.com`.

```json
{
  "error": "Suspicious activity detected.",
  "error_code": "suspicious_activity",
  "action": "bind_email",
  "action_url": "https://api.synorb.com/api/connect/bind-email",
  "verify": "If you are integrating legitimately, bind an email to verify your key: POST /api/connect/bind-email. Otherwise contact team@synorb.com.",
  "retry_guidance": "Do not retry anonymous provisioning. If you have an existing key, bind an email to begin verification; retry only after verification is confirmed or support directs you. Otherwise contact team@synorb.com.",
  "bind_email": "https://api.synorb.com/api/connect/bind-email",
  "contact_email": "team@synorb.com"
}
```

### Expired Synorb Key — HTTP 401

An expired key is distinct from an unknown or malformed key. Mint a fresh
72 Hour Agent Demo key or sign up. Do not retry the expired credential.

```json
{
  "error": "Synorb Key expired",
  "error_code": "synorb_key_expired",
  "action": "mint_new_demo_key",
  "action_url": "https://synorb.com/connect",
  "recovery": "Mint a new 72 Hour Agent Demo key at https://synorb.com/connect or sign up free at https://synorb.com/signup",
  "connect": "https://synorb.com/connect",
  "signup": "https://synorb.com/signup"
}
```

## Failure contracts

| Condition | Contract | Agent action |
| --- | --- | --- |
| Empty/missing scope | Can become a global visible-catalog query | Stop and resolve/use `stream_ids` |
| Unknown Tag | Empty result, zero usage | Resolve through `/ontology/tags` |
| Invalid/changed plan token | `409 invalid_plan_token`, zero usage | Re-plan the complete changed request |
| `planning_failed` | No execute, zero usage | Fix the typed scope/tag/date error and re-plan |
| `beacon_archived` | `409`, zero usage | Restore explicitly, then poll |
| `date_window_required` | `422`, zero usage | Add `lookback_hours` or a start/end date |
| `401 synorb_key_invalid` or `synorb_key_expired` | Actionable key-recovery envelope | Mint a fresh key at `/connect` or sign up at `/signup`; do not retry the same key |
| `404` | Actionable route/identifier envelope | Correct the request from `/docs` and `/openapi.json` before retrying |
| `429` | Rate-limit or usage envelope; inspect `detail.error_code` | For `rate_limited`, honor `Retry-After`; for terminal demo `quota_exhausted`, use `key_expires_at` and `starter_bind` (no `Retry-After`); for monthly exhaustion, wait for `quota_resets_on` |

| Pagination context | Contract | Agent action |
| --- | --- | --- |
| Catalog or Beacon page | Fully-qualified GET `next` / `prev` link, `null` at boundaries | GET the URL verbatim with the same authentication |
| Manifest query page (`mode` omitted or `"default"`) | Opaque fully-qualified GET `next` / `prev` URL, `null` at boundaries | GET the URL verbatim with the same authentication; the server-held cursor carries the query scope |
| Manifest query page (`mode="count"` or `"orient"`) | `next` / `prev` URL with no cursor -- a bare GET 405s | Re-POST your ORIGINAL request body to that exact URL; its `page_num`/`page_size` override the body's paging |
| Manifest exec page | POST paging hint in `next` / `prev`, `null` at boundaries | Copy the hinted paging values into the original body and re-POST; exec requires a fresh plan token for the changed body |

The unauthenticated `GET /health` probe is safe for discovery and readiness
checks. Synorb bounds it per client at the public probe limit. On successful
responses, it exposes `RateLimit-Policy`, `RateLimit`, and the compatibility
fields `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`. When a
client exceeds that probe limit, the probe returns `429` with `Retry-After`.
Authenticated content endpoints use their organization plan limits instead and
do not consume this public probe budget.
