# Inboxes, CI callbacks and delayed continuation
Download /docs/downloads/workflow_example.py beside /docs/downloads/agenthotel.py for an executed Python demonstration. Supply an authorized disposable standard project's origin, project ID and key through the environment variables in the quickstart. This creates retained resources, uses quota and performs no automatic retries. It requires inbox:manage, inbox:read, approval:write and schedule:write scopes. The standalone functions return resource identifiers without printing credentials.

## Create a callback inbox
POST /api/v1/projects/PROJECT_ID/inboxes with a scoped project key, Content-Type: application/json and a saved Idempotency-Key. Use a fresh UUIDv7 operation_id:
```json
{"operation_id":"YOUR_FRESH_UUID_V7","name":"CI callbacks","retention_seconds":86400,"max_payload_bytes":65536}
```
The initial 201 response returns id and a write_token shown once. Save that narrow capability securely. A CI runner needs only this write token to submit a callback, not the project key or root.

## Submit and retrieve an event
POST /hooks/v1/WRITE_TOKEN with Content-Type: application/json, a stable Idempotency-Key for that CI job, and its exact JSON result as the body. This capability is necessarily in the request path: never log or publish that path. No account Authorization header is needed. A 202 confirms durable acceptance; retain the returned sequence. Reusing the same key and exact payload does not create a second event. Changed input with that identity conflicts.

GET /api/v1/projects/PROJECT_ID/inboxes/INBOX_ID/events?after=0&limit=100 with a project key holding inbox:read. Continue with next_cursor when returned. Treat history_unavailable or cursor_expired explicitly; do not silently skip events. This read does not acknowledge a consumer.

## Approval and continuation
The downloadable demonstration creates an approval tied to an exact artifact reference, version and SHA-256, then schedules a one_shot event ten minutes later in the same inbox. Give the one-time approval capability to the authorized reviewer through your own secure channel. The scheduled event is a prompt to recheck, never permission to act: inspect the approval's terminal result and compare the current artifact against its approved binding. An unanswered, rejected, expired or mismatched approval cannot authorize the action.

Create a named consumer at POST /api/v1/projects/PROJECT_ID/inboxes/INBOX_ID/consumers with operation_id and name. Acknowledge only after your own durable processing by POSTing {"through_sequence":1} to /api/v1/projects/PROJECT_ID/inboxes/INBOX_ID/consumers/CONSUMER_ID/ack. Consumer positions are independent and monotonic.

## Go state example
Download /docs/downloads/state.go for a standard-library Go example. Call RoundTrip with a bounded context, your trusted TLS http.Client, HTTPS origin, project ID and authorized project key. It writes create-only exact JSON and returns unchanged response bytes; repeated execution on the same name conflicts deliberately. Do not log the credential or customer response.

Download /docs/downloads/freshness.go for the executed Go approval-freshness check. This example expects the exact option Approve and rejects mismatched references, versions or hashes. Establish trusted approval provenance first: arbitrary JSON delivered by another inbox writer is not a genuine approval. Keep using the checked bytes rather than reopening a mutable path.

## Capacity planning

POST /api/v1/projects/PROJECT_ID/usage/forecast with Authorization: Bearer PROJECT_KEY and Content-Type: application/json. The project key needs usage:read. No cookie, query parameter or idempotency key is needed. Send only an items array, with 1–64 items and at most1,000 total operations; the body is limited to32KiB.

```json
{"items":[{"kind":"approval","count":2,"content_plaintext_bytes":200,"allow_comment":true},{"kind":"context_standard","count":1,"descriptor_stored_bytes":100,"data_stored_bytes":10000}]}
```

Each item has kind and count. operation needs no sizes. encrypted_record requires content_plaintext_bytes for one stored envelope and one operation. approval requires content_plaintext_bytes and optionally allow_comment and artifact_plaintext_bytes. one_shot_schedule requires content_plaintext_bytes and all three completed_plaintext_bytes, cancelled_plaintext_bytes and expired_plaintext_bytes event lengths. recurring_schedule requires content_plaintext_bytes and payload_plaintext_bytes. context_standard and context_private require descriptor_stored_bytes and data_stored_bytes. Reject irrelevant fields even when zero or false.

Declare complete serialized stored sizes, including service wrappers, rather than HTTP body length or an unwrapped payload. Each non-context size is bounded at1MiB; descriptor size at8KiB and context data at20MiB+16 bytes. For private context, count client ciphertext including authentication tags. These are estimate-input bounds; your plan's file and JSON limits still apply to actual writes. Do not send content or encryption keys to estimate it.

The response echoes assumptions and includes encryption overhead, maximum approval comment/outcome storage and schedule outcome reservations. It reports used/reserved/available capacity with as_of, entitlement_version, plan_id and limits_version. Shortfalls include existing reservations. admission_guaranteed and later_recurring_runs_included are false: zero shortfall is not permission or a promise of acceptance, and later recurring runs are not reserved. Resource limits, concurrent changes and physical admission remain subject to actual acceptance checks. This never reserves capacity, quotes a price, pays or upgrades. Use the same REST route from clients that use MCP for other features.

Usage warnings are ok, near_limit at80% committed/reserved capacity, and at_limit when none remains. They do not trigger charges or change hard limits.

recent_hourly_usage reports workspace aggregate operations, outbound attempts and separate safety calls for the current and23 prior UTC hours. Empty hours may be omitted. Use the returned current-period totals for quota decisions; the hourly view is not historical billing evidence and does not identify projects, actions or content.

## Billing and a scheduled plan change

Billing is currently available only in the explicitly enabled, isolated test runtime. Fixture quotes are not live tax quotes. Use your account credential through protected Authorization configuration, with billing:read for status and billing:manage for changes; a project key does not confer billing authority. The Python and TypeScript clients' request method supports these REST routes without automatic retries.

Use the base path `/api/v1/accounts/ACCOUNT_ID/workspaces/WORKSPACE_ID/billing`. Read `/api/v1/billing/catalog` and current usage first. POST `BASE/quotes` with a saved UUIDv7 operation_id, action=purchase (or upgrade), plan_id, catalog_version, mode=prepaid or recurring and the current expected_entitlement_version. Read the returned final quote and exact quote_digest. If the complete amount, dates and recurring terms fit the authority you actually have, POST `BASE/orders` with a stable Idempotency-Key and operation_id, quote_id, quote_digest and explicit accept_renewal_terms. Never infer recurring permission from possession of a card.

For a nonzero order, prepare `BASE/customer`, then POST an empty object to `BASE/orders/ORDER_ID/prepare-checkout`. POST a saved operation_id to `BASE/orders/ORDER_ID/checkout-handoff` for the one-use secure browser handoff. Card details belong only on Stripe's supported secure surface. Provider authentication may require human involvement. Read `BASE/orders/ORDER_ID` for the durable result; a Checkout URL or return-page visit is not payment confirmation. A zero-price accepted upgrade skips Checkout.

For a recurring plan change at the next period boundary, first GET `BASE/future-terms`. Its future_terms_version is zero before the first acceptance. Request a fresh quote with action=renewal, the desired plan, mode=recurring and the current expected_entitlement_version. Accept that exact quote by POSTing to `BASE/future-terms`:

```json
{"operation_id":"YOUR_SAVED_UUID_V7","quote_id":"EXACT_QUOTE_ID","quote_digest":"EXACT_QUOTE_DIGEST","accept_renewal_terms":true,"expected_future_terms_version":0}
```

Use the version actually read, not a hard-coded zero. Retry only the same saved operation and exact body after an unknown response. Poll GET `BASE/future-terms`: accepted and installed are distinct, and neither grants paid access. The original current paid period remains unchanged; the next period still requires verified payment. If installation cannot be resolved, future renewal stops while the current paid period remains available. A newer quote does not replace accepted terms until explicitly accepted.

Read `BASE/subscriptions/ORDER_ID` for renewal status. POST `{}` to `BASE/subscriptions/ORDER_ID/cancel` to stop renewal. Purchased access remains through its original end, followed by seven days to retrieve affected data and 30 more days of locked retention before deletion. For invoice or receipt links, POST `{"cycle":0,"kind":"receipt"}` to `BASE/orders/ORDER_ID/document`; later renewal cycles use their own cycle number. These returned links are private capabilities, so do not log or publish them.
