@ovra/ts-sdk 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,381 +1,461 @@
1
- # Ovra TypeScript SDK
1
+ # `@ovra/ts-sdk`
2
2
 
3
- [![NPM version](https://img.shields.io/npm/v/@ovra/ts-sdk.svg)](https://www.npmjs.com/package/@ovra/ts-sdk)
4
- [![NPM downloads](https://img.shields.io/npm/dm/@ovra/ts-sdk.svg)](https://www.npmjs.com/package/@ovra/ts-sdk)
3
+ [![npm](https://img.shields.io/npm/v/@ovra/ts-sdk?label=npm&color=2bc760)](https://www.npmjs.com/package/@ovra/ts-sdk)
4
+ [![downloads](https://img.shields.io/npm/dm/@ovra/ts-sdk?color=2bc760)](https://www.npmjs.com/package/@ovra/ts-sdk)
5
+ [![bundle](https://img.shields.io/bundlephobia/minzip/@ovra/ts-sdk?label=size)](https://bundlephobia.com/package/@ovra/ts-sdk)
6
+ [![types](https://img.shields.io/npm/types/@ovra/ts-sdk?color=3178c6)](https://www.npmjs.com/package/@ovra/ts-sdk)
5
7
 
6
- The official TypeScript SDK for the [Ovra API](https://getovra.com) payment
7
- infrastructure for AI agents. Issue virtual Visa cards, declare and approve
8
- spending intents, settle payments under signed mandates, and reconcile from
9
- the same typed surface.
8
+ > Payment infrastructure for AI agents. Mint virtual Visa cards bound to
9
+ > AP2 mandate chains, settle in one call, never see a PAN.
10
10
 
11
- **Version: `0.4.1`** — fully bound to the `/v1/*` API. Auto-published
12
- from `apps/api/openapi.json` on every push to `main` that touches the
13
- spec or `packages/sdk/src`; see the repo
14
- [CHANGELOG](https://github.com/ovra/ovra/blob/main/CHANGELOG.md) for the
15
- v0 → v1 migration notes.
11
+ ```ts
12
+ import { Ovra } from "@ovra/ts-sdk";
13
+
14
+ const ovra = new Ovra({ apiKey: process.env.OVRA_API_KEY! });
15
+
16
+ const result = await ovra.pay({
17
+ agentId: "agt_…", // an active agent in your workspace
18
+ offerId: "aic_openai-1000", // catalog offer id
19
+ merchant: "OpenAI",
20
+ amount: 10.0, // €10.00
21
+ purpose: "Top up OpenAI credits",
22
+ });
16
23
 
17
- **0.4.0 is a breaking release.** The `/v1/credentials/*` and
18
- `/v1/authorization-grants/*` rails were removed (alongside MCP
19
- `ovra_pay`); the AP2 agentic-commerce buy is now the canonical payment
20
- flow. The `ovra.credentials` namespace is gone — use
21
- `ovra.agenticCommerce.{search, buy, getOrder, listOrders, verifyOrder,
22
- refundOrder}` instead. See "Agentic Commerce surface" below.
24
+ if (result.status === "completed") {
25
+ console.log(result.orderId); // cmo_…
26
+ console.log(result.order.amount); // { amount: 1000, currency: "eur" }
27
+ console.log(result.order.authorization.network_auth_code); // "000123"
28
+ console.log(result.order.receipt.id); // vrcpt_…
29
+ }
30
+ ```
31
+
32
+ One call. The SDK declares the intent, runs the policy gate, debits
33
+ the wallet, mints the DPAN, signs the 4-mandate AP2 chain, captures
34
+ with the merchant, and returns the order envelope.
35
+
36
+ ---
37
+
38
+ ## What you get
39
+
40
+ - **Typed end-to-end** — every request body, response envelope, and error
41
+ code is generated from the live OpenAPI spec. No `any` leaking.
42
+ - **One-call payments** — `ovra.pay()` for the AP2 happy path, or drop
43
+ to primitives (`agents`, `intents`, `agenticCommerce.buy`) when you
44
+ need control.
45
+ - **Card data never reaches your code** — PAN/CVV stay inside Ovra;
46
+ you get `last4` + a single-use DPAN + cryptogram per transaction.
47
+ - **33 namespaces** — agents, cards, wallets, intents, transactions,
48
+ agentic-commerce, refunds, webhooks, audit, billing, and 23 more.
49
+ - **Idempotent by default** — every mutating call mints a UUID
50
+ Idempotency-Key; safe to retry on network failure.
51
+ - **Sender-agnostic** — Node 20+, Bun, Deno, Cloudflare Workers,
52
+ Vercel Edge, any runtime with `fetch`.
53
+
54
+ ---
23
55
 
24
56
  ## Install
25
57
 
26
58
  ```sh
27
- npm install @ovra/ts-sdk
59
+ npm i @ovra/ts-sdk # or pnpm add / yarn add / bun add
28
60
  ```
29
61
 
30
- Requirements: Node.js 20+, or any runtime with `fetch` (Bun, Deno,
31
- Cloudflare Workers, Vercel Edge).
62
+ Get a `sk_test_*` key from `app.getovra.com` Settings → API keys.
32
63
 
33
- ## Quickstart
64
+ ---
34
65
 
35
- Grab an API key at [getovra.com/dashboard](https://getovra.com/dashboard) →
36
- Settings → API keys, then drop straight into the canonical **one-call buy**:
66
+ ## 60-second tour
67
+
68
+ ### The one-call AP2 purchase (recommended)
37
69
 
38
70
  ```ts
39
71
  import { Ovra } from "@ovra/ts-sdk";
40
72
 
41
73
  const ovra = new Ovra({ apiKey: process.env.OVRA_API_KEY! });
42
74
 
43
- // `ovra.pay()` wraps the AP2 agentic-commerce buy — DPAN mint + the
44
- // 4-mandate AP2 chain + capture + receipt in one round-trip. Amount is
45
- // EUR (decimal); the SDK converts to cents on the wire.
46
- const result = await ovra.pay({
47
- agentId: "agt_…", // an active agent in your workspace
48
- offerId: "off_…", // catalog offer id (see ovra.agenticCommerce.search)
49
- merchant: "Notion",
50
- amount: 79.0, // €79.00
51
- purpose: "Monthly Notion Team Plan renewal",
75
+ // 1. Discover an offer
76
+ const { data: offers } = await ovra.agenticCommerce.search({
77
+ body: { vertical: "ai-credits", query: { provider: "openai", amount_usd: 10 } },
52
78
  });
53
79
 
54
- if (result.status === "completed") {
55
- console.log(result.orderId, result.order.authorization.network_auth_code);
56
- } else {
57
- console.error("buy failed:", result.reason);
58
- }
59
- ```
60
-
61
- Prefer the raw operation? Same call, different envelope shape:
62
-
63
- ```ts
64
- const { data: order } = await ovra.agenticCommerce.buy({
65
- body: {
66
- agent_id: "agt_…",
67
- offer_id: "off_…",
68
- purpose: "Monthly Notion Team Plan renewal",
69
- },
80
+ // 2. Buy it — one call covers intent + policy + wallet + DPAN + AP2 chain + capture
81
+ const order = await ovra.pay({
82
+ agentId: "agt_…",
83
+ offerId: offers[0].id,
84
+ merchant: offers[0].merchant.name,
85
+ amount: offers[0].amountCents / 100,
70
86
  });
71
- console.log(order.order_id, order.status);
87
+
88
+ console.log(order.orderId); // cmo_…
89
+ console.log(order.status); // "completed" | "pending_approval" | "failed"
90
+ console.log(order.order?.receipt?.id); // vrcpt_…
91
+ console.log(order.order?.mandate_chain); // { open_checkout, open_payment, closed_checkout, closed_payment }
72
92
  ```
73
93
 
74
- Need the lower-level intent / checkout pipeline (custom approval UI,
75
- out-of-band capture)? The primitives are still here:
94
+ ### Reading + reconciling
76
95
 
77
96
  ```ts
78
- // 1. Provision an agent. `policy` is required — fetch one via
79
- // `ovra.policies.list()`. `profile.purpose` is required.
80
- const { data: agent } = await ovra.agents.create({
81
- body: {
82
- name: "Notion Buyer",
83
- policy: "po_…",
84
- profile: { purpose: "Monthly Notion Team Plan renewal" },
85
- },
86
- });
97
+ // List orders (cursor-paginated, auto-unwrapped)
98
+ const orders = await ovra.agenticCommerce.listOrders();
99
+ for (const o of orders.data) console.log(o.id, o.status, o.totals);
87
100
 
88
- // 2. Declare a spending intent (`agent`, not `agent_id`).
89
- const { data: intent } = await ovra.intents.create({
90
- body: {
91
- agent: agent.id,
92
- merchant: "Notion",
93
- amount: { amount: 7900, currency: "eur" }, // €79.00
94
- purpose: "Monthly Notion Team Plan renewal",
95
- },
96
- });
101
+ // Get one with the full UCP envelope (line_items + fulfillment + adjustments)
102
+ const detail = await ovra.agenticCommerce.getOrder({ path: { order_id: "cmo_…" } });
103
+
104
+ // Verify the AP2 chain integrity server-side
105
+ const verified = await ovra.agenticCommerce.verifyOrder({ path: { order_id: "cmo_…" } });
106
+ if (!verified.data.valid) console.warn("chain tampered:", verified.data.reasons);
107
+ ```
97
108
 
98
- // 3. Approve the intent (production: this comes from the human
99
- // approving in the dashboard or a passkey assertion).
100
- await ovra.intents.approve({ path: { id: intent.id } });
109
+ ### Refunds
101
110
 
102
- // 4. Execute the checkout — `intent` + `target_url` (the merchant URL
103
- // the DPAN will be submitted to) are both required.
104
- const { data: result } = await ovra.checkout.execute({
105
- body: {
106
- intent: intent.id,
107
- target_url: "https://www.notion.so/checkout",
108
- },
111
+ ```ts
112
+ // Partial refund order goes COMPLETED COMPENSATED
113
+ await ovra.agenticCommerce.refundOrder({
114
+ path: { order_id: "cmo_…" },
115
+ body: { amount_cents: 500, reason: "customer cancellation" },
109
116
  });
110
117
 
111
- console.log(result.transaction_id, result.status);
118
+ // Or full refund (no body) — order goes COMPLETED → REFUNDED
119
+ await ovra.agenticCommerce.refundOrder({ path: { order_id: "cmo_…" } });
112
120
  ```
113
121
 
114
- Amounts on the wire are integers in the currency's minor units
115
- (`amount: 7900` = €79.00). Card PAN / CVV are **never** returned — only
116
- `last4` and a tokenized DPAN reach your code.
122
+ ---
117
123
 
118
- ## Authentication
124
+ ## Auth scopes
119
125
 
120
- The constructor accepts any Ovra credential prefix. Pick the smallest scope
121
- that fits the call site.
126
+ The constructor takes any Ovra credential prefix. Pick the smallest scope that fits.
122
127
 
123
- | Prefix | What it is | Use when |
124
- | ------------------- | ---------------------- | ------------------------------------------------------------------- |
125
- | `sk_live_…` | Full workspace key | Server-side production code that needs to mint/rotate/manage. |
126
- | `sk_test_…` | Full key, sandbox mode | Local dev and CI; `livemode: false` on every response. |
127
- | `rk_…` / `rk_test_…` | Restricted key | Read + scoped writes; blocked from key, webhook, billing, policy mutations. Safe to embed in less-trusted runtimes. |
128
- | `at_…` | Agent token | Scoped to one agent with a hard spend cap. The credential you ship to an agent runtime. |
128
+ | Prefix | What | When |
129
+ | -------------- | --------------------- | -------------------------------------------------------------------------- |
130
+ | `sk_live_…` | Full live key | Server-side prod code that mints / rotates / manages. |
131
+ | `sk_test_…` | Full sandbox key | Local dev + CI. Every envelope returns `livemode: false`. |
132
+ | `rk_…` | Restricted key | Read + scoped writes. Blocked from keys / webhooks / billing / policies. |
133
+ | `at_…` | Agent token | Bound to one agent with a hard spend cap. The credential you ship to the agent runtime. |
129
134
 
130
135
  ```ts
131
136
  const ovra = new Ovra({
132
137
  apiKey: process.env.OVRA_API_KEY!,
133
- // Sent as the `X-Ovra-Client-App` header on every request — used for
134
- // analytics and to help support trace issues back to your integration.
135
- appInfo: { name: "AcmeCRM", version: "1.4.2" },
138
+ appInfo: { name: "AcmeCRM", version: "1.4.2" }, // X-Ovra-Client-App header
136
139
  });
137
140
 
138
- // Best-effort scope hint derived from the key prefix — useful for
139
- // dev-time logging only; the server is the source of truth.
140
- // sk_* → "full" | rk_* → "restricted" | at_* → "agent"
141
- console.log(ovra.keyScope);
141
+ console.log(ovra.keyScope); // "full" | "restricted" | "agent" | "unknown" (dev-time hint)
142
142
  ```
143
143
 
144
- Test-mode keys produce objects with `livemode: false`; live keys produce
145
- `livemode: true`. Whenever the API is deployed against the sandbox card
146
- issuer, **every** response is `livemode: false` regardless of key — the
147
- whole workspace is sandbox until the real issuer ships.
148
-
149
- ## Money
150
-
151
- Money is `{ amount, currency }` everywhere on the wire. `amount` is an
152
- integer in minor units; `currency` is a three-letter ISO code lowercased.
153
-
154
- | Display | Wire value |
155
- | ------------ | ----------------------------------------- |
156
- | €50.00 | `{ amount: 5000, currency: "eur" }` |
157
- | €0.99 | `{ amount: 99, currency: "eur" }` |
158
- | £1,234.56 | `{ amount: 123456, currency: "gbp" }` |
159
- | ¥1,000 (JPY) | `{ amount: 1000, currency: "jpy" }` (JPY has no minor units; treat the integer as the whole-yen amount.) |
160
-
161
- Floats and decimal strings are rejected at the validation layer. There is
162
- no implicit unit conversion — what you send is what gets compared against
163
- the policy.
164
-
165
- ## Timestamps
166
-
167
- Every timestamp is an ISO 8601 string in the `created` field
168
- (`"2026-05-25T18:04:16Z"`). The legacy `created_at` / `createdAt` / epoch
169
- millis variants are gone.
144
+ ---
170
145
 
171
146
  ## Errors
172
147
 
173
- API errors throw a typed subclass of `OvraError`. Branch on type, status,
174
- or preferred `code`:
148
+ Typed exceptions per error category branch on `e.code` (machine-readable),
149
+ show `e.message` to humans, link to `e.docUrl` from your own UI.
175
150
 
176
151
  ```ts
177
152
  import {
178
153
  Ovra,
179
154
  OvraError,
155
+ OvraAuthenticationError,
156
+ OvraPermissionError,
157
+ OvraPaymentRequiredError,
158
+ OvraConflictError,
180
159
  OvraNotFoundError,
181
- OvraRateLimitError,
182
160
  } from "@ovra/ts-sdk";
183
161
 
184
162
  try {
185
- await ovra.cards.get({ path: { id: "crd_…" } });
186
- } catch (err) {
187
- if (err instanceof OvraNotFoundError) {
188
- // 404 — card doesn't exist or isn't yours
189
- } else if (err instanceof OvraRateLimitError) {
190
- // 429 — back off and retry
191
- } else if (err instanceof OvraError && err.code === "E_POLICY_DENIED") {
192
- // policy gate refused the spend; `err.message` is the human string,
193
- // `err.param` points at the offending body field if any.
194
- } else {
195
- throw err;
163
+ await ovra.pay({ agentId, offerId, merchant, amount });
164
+ } catch (e) {
165
+ if (!(e instanceof OvraError)) throw e;
166
+
167
+ switch (e.code) {
168
+ case "E_INSUFFICIENT_FUNDS": return refillAndRetry(e.message);
169
+ case "E_POLICY_LIMIT_EXCEEDED": return showLimits(e.escalationReason);
170
+ case "E_OFFER_EXPIRED": return reSearch();
171
+ case "E_MANDATE_BINDING_FAIL": return audit(e.requestId);
172
+ case "E_FSM_INVALID_TRANSITION": return refreshState();
173
+ default: return showError(e.message, e.docUrl);
196
174
  }
197
175
  }
198
176
  ```
199
177
 
200
- On the wire, every error body matches the v1 envelope:
178
+ Every error envelope carries `code`, `type`, `message`, `param`, `docUrl`,
179
+ `requestId`, and `status`. See the [error reference](https://app.getovra.com/docs/errors).
201
180
 
202
- ```json
203
- {
204
- "error": {
205
- "type": "policy_error",
206
- "code": "E_POLICY_DENIED",
207
- "message": "Spend exceeds the configured policy limit.",
208
- "param": "amount",
209
- "doc_url": "https://docs.getovra.com/errors/E_POLICY_DENIED",
210
- "request_id": "req_…",
211
- "status": 403
212
- }
213
- }
214
- ```
215
-
216
- The thrown `OvraError` flattens that envelope onto the instance —
217
- `err.type`, `err.code`, `err.message`, `err.param`, `err.docUrl`,
218
- `err.requestId`, `err.status`. Branch on `err.code` — it's typed,
219
- ~50 canonical values, stable across releases. Never parse `err.message`
220
- — it's a human string and may change.
181
+ ---
221
182
 
222
183
  ## Idempotency
223
184
 
224
- Mutating requests (`POST`, `PATCH`, `DELETE`) require an `Idempotency-Key`
225
- header. **The SDK auto-mints a fresh UUID per call**, so retries through
226
- the built-in retry policy are safe by default. Override per-call when you
227
- want explicit deduplication across processes:
185
+ Every mutating call (POST / PATCH / DELETE) is idempotent by default — the
186
+ SDK mints a UUID `Idempotency-Key` per logical operation and reuses it
187
+ across retries. Replays inside the 24h dedup window return the original
188
+ response with `X-Idempotent-Replayed: true`.
189
+
190
+ Bring your own key when you need cross-process dedup:
228
191
 
229
192
  ```ts
230
- await ovra.transfers.create({
231
- headers: { "Idempotency-Key": "transfer-2026-q2-invoice-4711" },
232
- body: {
233
- from_wallet: "wal_…",
234
- to_beneficiary: "ben_…",
235
- amount: { amount: 250000, currency: "eur" },
236
- },
193
+ await ovra.intents.create({
194
+ body: { agent: "agt_…", merchant: "Notion", amount: { amount: 7900, currency: "eur" }, purpose: "…" },
195
+ headers: { "Idempotency-Key": "your-stable-key" },
237
196
  });
238
197
  ```
239
198
 
240
- Re-sending the same key + identical body returns the original response.
241
- Re-sending the same key with a *different* body returns
242
- `E_IDEMPOTENCY_CONFLICT` (HTTP 409). An in-flight collision returns
243
- `E_IDEMPOTENCY_IN_FLIGHT`.
199
+ ---
244
200
 
245
201
  ## Pagination
246
202
 
247
- Every list method is cursor-paginated and returns the canonical envelope
248
- directly — the SDK auto-strips hey-api's outer `{data, request, response}`
249
- wrapper for any `list*` call so you get the list shape verbatim:
203
+ List endpoints return `{ data, has_more, next_cursor }` (auto-unwrapped from
204
+ the hey-api envelope). Walk pages with `next_cursor`:
250
205
 
251
206
  ```ts
252
- const page1 = await ovra.cards.list({ query: { limit: 50 } });
253
- // ^? { object: "list", data: Card[], has_more, next_cursor, url }
207
+ let cursor: string | undefined;
208
+ do {
209
+ const page = await ovra.transactions.list({ query: { limit: 100, starting_after: cursor } });
210
+ for (const tx of page.data) process(tx);
211
+ cursor = page.has_more ? page.next_cursor : undefined;
212
+ } while (cursor);
213
+ ```
254
214
 
255
- for (const card of page1.data) {
256
- console.log(card.id, card.last4);
257
- }
215
+ Default `limit` is 50, max 100. Bad cursors return `400 E_VALIDATION` —
216
+ no silent full-list fallback.
258
217
 
259
- if (page1.has_more) {
260
- const page2 = await ovra.cards.list({
261
- query: { limit: 50, starting_after: page1.next_cursor! },
262
- });
263
- }
264
- ```
218
+ ---
265
219
 
266
- Or use the `paginate` helper to iterate the full set:
220
+ ## Money
221
+
222
+ Wire amounts are integer cents inside a Money object: `{ amount, currency }`.
267
223
 
268
224
  ```ts
269
- import { paginate } from "@ovra/ts-sdk";
225
+ import { parseMoney, formatMoney } from "@ovra/ts-sdk";
270
226
 
271
- for await (const card of paginate(
272
- (args) => ovra.cards.list({ query: args }),
273
- { limit: 50 },
274
- )) {
275
- console.log(card.id, card.last4);
276
- }
227
+ parseMoney(79.0) // { amount: 7900, currency: "eur" }
228
+ parseMoney("79.00", "usd") // { amount: 7900, currency: "usd" }
229
+ formatMoney({ amount: 7900, currency: "eur" }, "de-DE") // "79,00 €"
277
230
  ```
278
231
 
279
- The wire envelope (what `page1` deserializes to):
232
+ `ovra.pay()` accepts decimals (`amount: 79.0` = €79.00) and converts at the
233
+ boundary. Lower-level `agents/intents/transactions` operate on Money objects
234
+ directly.
280
235
 
281
- ```json
282
- {
283
- "object": "list",
284
- "data": [ /* … */ ],
285
- "has_more": true,
286
- "next_cursor": "card_01HE…",
287
- "url": "/v1/cards"
288
- }
289
- ```
236
+ ---
290
237
 
291
- ## Retries
238
+ ## Resource matrix
292
239
 
293
- Connection errors and 5xx / 429 / 408 responses retry automatically with
294
- jittered exponential backoff (500 ms → 4 s, 30 s wall-clock budget).
295
- Tune or disable per-client:
240
+ 33 typed namespaces. Scope + plan gating shown.
296
241
 
297
- ```ts
298
- const ovra = new Ovra({
299
- apiKey: "…",
300
- retry: { maxRetries: 5, maxElapsedMs: 60_000 },
301
- // or: retry: false
302
- });
303
- ```
242
+ <details><summary><b>Money &amp; orders</b> (8 namespaces) — click to expand</summary>
243
+
244
+ | Namespace | Ops | Scope | Plan |
245
+ | ------------------------ | -------------------------------------------------- | :---: | :--: |
246
+ | `wallets` | list / get / fund / topup | any | free |
247
+ | `transactions` | list / get / memo | any | free |
248
+ | `intents` | create / list / get / approve / cancel / verify | any | free |
249
+ | `agenticCommerce` | listVerticals / search / getOffer / buy / listOrders / getOrder / verifyOrder / refundOrder | any | free |
250
+ | `ledgerEntries` | list | full | free |
251
+ | `invoices` | list / get | full | free |
252
+ | `billing` | getBalance / addPaymentMethod | full | free |
253
+
254
+ </details>
255
+
256
+ <details><summary><b>Agents &amp; spending</b> (7 namespaces)</summary>
257
+
258
+ | Namespace | Ops | Scope | Plan |
259
+ | ------------------ | ----------------------------------------------------------------------- | :---: | :------: |
260
+ | `agents` | create / list / get / update / attest / freeze / unfreeze | any | free |
261
+ | `cards` | issue / list / get / freeze / unfreeze / rotate / close | any | free |
262
+ | `policies` | list / get / create / update / delete / assign | full | free |
263
+ | `delegations` | create / list / get / redeem / revoke | full | pro |
264
+ | `approvalPolicies` | list / get / create / update / delete | full | business |
265
+ | `costAllocations` | list / create / assign / delete | full | pro |
266
+ | `spendAnomalies` | list / ack / dismiss | full | pro |
267
+
268
+ </details>
269
+
270
+ <details><summary><b>Compliance &amp; governance</b> (5 namespaces)</summary>
271
+
272
+ | Namespace | Ops | Scope | Plan |
273
+ | ------------------ | ----------------------------------------- | :---: | :------: |
274
+ | `riskEvents` | list | full | business |
275
+ | `disputeEvidence` | list / upload / get | any | free |
276
+ | `auditEvents` | list | full | business |
277
+ | `accessEvents` | list | any | free |
278
+ | `metrics` | list | full | business |
279
+
280
+ </details>
281
+
282
+ <details><summary><b>Identity &amp; access</b> (3 namespaces)</summary>
283
+
284
+ | Namespace | Ops | Scope | Plan |
285
+ | -------------------- | ------------------------------------ | :---: | :--: |
286
+ | `customers` | get / update / gdpr_* (dashboard) | full | free |
287
+ | `apiKeys` | list / create / revoke | full | free |
288
+ | `merchantCategories` | list / explain | any | free |
289
+
290
+ </details>
291
+
292
+ <details><summary><b>Webhooks &amp; notifications</b> (3 namespaces)</summary>
304
293
 
305
- ## Agentic Commerce surface
294
+ | Namespace | Ops | Scope | Plan |
295
+ | ------------------- | ---------------------------------------------------- | :---: | :--: |
296
+ | `webhooks` | list / create / get / update / delete / rotate-secret| full | free |
297
+ | `notifications` | list / unread_count / mark_read / mark_all_read | any | free |
298
+ | `pushSubscriptions` | subscribe / unsubscribe / vapidPublicKey | any | free |
306
299
 
307
- The AP2 sandbox is the canonical buy pipeline as of 0.4.0. Catalog
308
- discovery, the buy orchestrator, and the UCP-shaped order envelope
309
- (line_items / fulfillment / adjustments / totals / permalink_url) all
310
- live under `ovra.agenticCommerce`:
300
+ </details>
301
+
302
+ > **Want a different namespace?** Generated functions for every operation
303
+ > live in `@ovra/ts-sdk/api` — bypass the bound facade and call them
304
+ > directly. The bindings exist for ergonomics; nothing's hidden.
305
+
306
+ ---
307
+
308
+ ## Recipes
309
+
310
+ <details><summary><b>Manual intent → approve → checkout</b></summary>
311
311
 
312
312
  ```ts
313
- // Browse the 10-vertical catalog
314
- const offers = await ovra.agenticCommerce.search({
315
- body: { vertical: "saas-subscriptions", query: { merchant: "notion", plan: "team" } },
313
+ // 1. Create an agent (policy is required; profile.purpose is required)
314
+ const { data: agent } = await ovra.agents.create({
315
+ body: {
316
+ name: "Notion Buyer",
317
+ policy: "po_…",
318
+ profile: { purpose: "Monthly Notion Team Plan renewal" },
319
+ },
316
320
  });
317
321
 
318
- // One-call buy mints DPAN + signs the 4-mandate AP2 chain + captures.
319
- // The route reads `agent_id`, `offer_id`, `purpose`, optional `card_id`
320
- // + `intent_id`. Amount + merchant are derived from the offer.
321
- const order = await ovra.agenticCommerce.buy({
322
+ // 2. Declare a spending intent (field is `agent`, not `agent_id`)
323
+ const { data: intent } = await ovra.intents.create({
322
324
  body: {
323
- agent_id: "agt_…",
324
- offer_id: "off_…",
325
+ agent: agent.id,
326
+ merchant: "Notion",
327
+ amount: { amount: 7900, currency: "eur" },
325
328
  purpose: "Monthly Notion Team Plan renewal",
326
329
  },
327
330
  });
328
331
 
329
- // Re-verify the AP2 chain against the cached mandate set
330
- const check = await ovra.agenticCommerce.verifyOrder({
331
- path: { order_id: order.data.order_id },
332
- });
332
+ // 3. Approve (production: passkey ceremony; sandbox: viaFixture=true)
333
+ await ovra.intents.approve({ path: { id: intent.id }, body: { confirm: true } });
333
334
 
334
- // UCP-conformant refundappend to adjustments, debit totals, flip
335
- // status to REFUNDED (full) or COMPENSATED (partial)
336
- await ovra.agenticCommerce.refundOrder({
337
- path: { order_id: order.data.order_id },
338
- body: { amount_cents: 7900, reason: "customer cancelled" },
335
+ // 4. Execute the checkout `intent` + `target_url` both required
336
+ const { data: result } = await ovra.checkout.execute({
337
+ body: { intent: intent.id, target_url: "https://www.notion.so/checkout" },
339
338
  });
339
+ console.log(result.transaction_id, result.status);
340
340
  ```
341
341
 
342
- ## Resource coverage
342
+ </details>
343
343
 
344
- `Ovra` binds the highest-traffic resources as ergonomic namespaces. The
345
- full v1 surface (103 paths, ~145 operations) is also available as
346
- tree-shakable functions via `import { … } from "@ovra/ts-sdk/api"` —
347
- use those directly for the long-tail resources not in the table below.
344
+ <details><summary><b>List + reconcile transactions for one agent</b></summary>
348
345
 
349
- Bound on the `Ovra` instance:
346
+ ```ts
347
+ let cursor: string | undefined;
348
+ const all: Transaction[] = [];
349
+ do {
350
+ const page = await ovra.transactions.list({
351
+ query: { agent: "agt_…", limit: 100, starting_after: cursor },
352
+ });
353
+ all.push(...page.data);
354
+ cursor = page.has_more ? page.next_cursor : undefined;
355
+ } while (cursor);
356
+
357
+ const totalCents = all
358
+ .filter(t => t.status === "completed")
359
+ .reduce((sum, t) => sum + t.amount.amount, 0);
360
+ ```
350
361
 
351
- - **Agents & auth** — `agents`, `passkeys`
352
- - **Spending** — `intents`, `cards`, `checkout`, `agenticCommerce`,
353
- `paymentRequests`
354
- - **Money movement** — `wallets`, `transfers`, `beneficiaries`, `refunds`
355
- - **Policy & governance** — `policies`, `disputes`
356
- - **Observability** — `transactions`, `outcomes`, `runs`, `forecast`
357
- - **Identity** — `customers`, `webhooks`
358
- - **One-call buy** — `pay()` (wraps `agenticCommerce.buy`)
362
+ </details>
359
363
 
360
- Need `apiKeys`, `delegations`, `approvalPolicies`, `riskEvents`,
361
- `auditEvents`, `accessEvents`, `notifications`, `invoices`, `metrics`,
362
- `merchantCategories`, `pushSubscriptions`, `ledgerEntries`,
363
- `disputeEvidence`, or `billing`? Import the generated function and pass
364
- the client:
364
+ <details><summary><b>Register a webhook + handle delivery</b></summary>
365
365
 
366
366
  ```ts
367
- import { Ovra } from "@ovra/ts-sdk";
368
- import { listApiKeys } from "@ovra/ts-sdk/api";
367
+ // One-time setup
368
+ const { data: hook } = await ovra.webhooks.create({
369
+ body: {
370
+ url: "https://app.example.com/ovra/webhook",
371
+ events: ["intent.approved", "transaction.completed", "order.refunded"],
372
+ },
373
+ });
374
+ console.log("save this secret:", hook.signing_secret);
375
+
376
+ // In your HTTP handler — verify signature with the shared secret.
377
+ // Ovra uses HMAC-SHA256 over the raw body (Stripe-style):
378
+ // sig = hex( hmac_sha256(secret, raw_body) )
379
+ // compare in constant time against the `X-Ovra-Signature` header
380
+ import { createHmac, timingSafeEqual } from "node:crypto";
381
+
382
+ const expected = createHmac("sha256", process.env.OVRA_WEBHOOK_SECRET!)
383
+ .update(rawBody)
384
+ .digest("hex");
385
+ const got = (req.headers["x-ovra-signature"] as string).replace(/^sha256=/, "");
386
+ if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(got, "hex"))) {
387
+ return res.status(401).send("bad signature");
388
+ }
389
+ ```
369
390
 
370
- const ovra = new Ovra({ apiKey: process.env.OVRA_API_KEY! });
371
- // (advanced) reach into the generated client for unbound namespaces:
372
- const keys = await listApiKeys({ query: { limit: 10 } });
391
+ </details>
392
+
393
+ <details><summary><b>Issue a delegation (sub-agent / B2B2B)</b></summary>
394
+
395
+ ```ts
396
+ const { data: delegation } = await ovra.delegations.create({
397
+ body: {
398
+ grantor_agent: "agt_…",
399
+ purpose: "Marketing team — quarterly spend",
400
+ spend_cap_cents: 500_00,
401
+ ttl_seconds: 60 * 60 * 24 * 90, // 90 days
402
+ },
403
+ });
404
+ // Hand `delegation.redemption_url` to the delegate; they exchange for an at_*
405
+ ```
406
+
407
+ </details>
408
+
409
+ ---
410
+
411
+ ## MCP server
412
+
413
+ Prefer using Ovra straight from Claude / Cursor / any MCP-capable agent?
414
+ The same surface lives in [`@ovra/mcp`](https://www.npmjs.com/package/@ovra/mcp)
415
+ (10 tools). Add to `.mcp.json`:
416
+
417
+ ```json
418
+ {
419
+ "mcpServers": {
420
+ "ovra": {
421
+ "command": "npx",
422
+ "args": ["-y", "@ovra/mcp"],
423
+ "env": { "OVRA_API_KEY": "sk_test_…" }
424
+ }
425
+ }
426
+ }
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Publishing
432
+
433
+ <details><summary>How releases ship (npm + GitHub Actions)</summary>
434
+
435
+ The SDK is regenerated from `apps/api/openapi.json` whenever the spec or
436
+ `packages/sdk/src` changes. The publish workflow at
437
+ `.github/workflows/sdk-publish.yaml` is wired but currently **blocked on
438
+ GitHub Actions billing**. Until that's resolved, releases ship manually:
439
+
440
+ ```sh
441
+ # From the repo root
442
+ pnpm run publish:sdk # builds, tests, publishes (refuses dirty tree)
443
+ pnpm run publish:sdk:dry # rehearsal — no actual npm publish
373
444
  ```
374
445
 
375
- The full inventory is in `apps/api/openapi.json` (single source of
376
- truth — the SDK is regenerated from it on every release).
446
+ Or directly: `cd packages/sdk && pnpm run publish-manual`.
447
+
448
+ The script enforces:
449
+ - Clean working tree (refuses to publish over uncommitted changes)
450
+ - Local version > current npm `latest`
451
+ - Valid `npm whoami` (must be authenticated)
452
+ - Regen + build + tests must all pass before `npm publish`
453
+
454
+ </details>
455
+
456
+ ---
377
457
 
378
458
  ## License
379
459
 
380
- Commercialsee [LICENSE](./LICENSE). The Ovra service requires a paid
381
- subscription.
460
+ UNLICENSEDinternal use only until the public release lands. See the
461
+ repo for the in-progress [terms](https://github.com/ovra/ovra/blob/main/LICENSE).