@ovra/ts-sdk 0.5.0 → 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.
Files changed (2) hide show
  1. package/README.md +340 -375
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,496 +1,461 @@
1
- # Ovra TypeScript SDK
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)
5
-
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.
10
-
11
- **Version: `0.5.0`** — fully bound to the `/v1/*` API. Regenerated
12
- from `apps/api/openapi.json`; releases ship manually for now (see
13
- [Publishing](#publishing) below — auto-publish is wired but pending
14
- on GH Actions billing). See the repo
15
- [CHANGELOG](https://github.com/ovra/ovra/blob/main/CHANGELOG.md) for the
16
- v0 → v1 migration notes.
17
-
18
- **0.4.0 is a breaking release.** The `/v1/credentials/*` and
19
- `/v1/authorization-grants/*` rails were removed (alongside MCP
20
- `ovra_pay`); the AP2 agentic-commerce buy is now the canonical payment
21
- flow. The `ovra.credentials` namespace is gone — use
22
- `ovra.agenticCommerce.{search, buy, getOrder, listOrders, verifyOrder,
23
- refundOrder}` instead. See "Agentic Commerce surface" below.
1
+ # `@ovra/ts-sdk`
24
2
 
25
- ## Install
26
-
27
- ```sh
28
- npm install @ovra/ts-sdk
29
- ```
30
-
31
- Requirements: Node.js 20+, or any runtime with `fetch` (Bun, Deno,
32
- Cloudflare Workers, Vercel Edge).
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)
33
7
 
34
- ## Quickstart
35
-
36
- Grab an API key at [getovra.com/dashboard](https://getovra.com/dashboard) →
37
- Settings → API keys, then drop straight into the canonical **one-call buy**:
8
+ > Payment infrastructure for AI agents. Mint virtual Visa cards bound to
9
+ > AP2 mandate chains, settle in one call, never see a PAN.
38
10
 
39
11
  ```ts
40
12
  import { Ovra } from "@ovra/ts-sdk";
41
13
 
42
14
  const ovra = new Ovra({ apiKey: process.env.OVRA_API_KEY! });
43
15
 
44
- // `ovra.pay()` wraps the AP2 agentic-commerce buy — DPAN mint + the
45
- // 4-mandate AP2 chain + capture + receipt in one round-trip. Amount is
46
- // EUR (decimal); the SDK converts to cents on the wire.
47
16
  const result = await ovra.pay({
48
- agentId: "agt_…", // an active agent in your workspace
49
- offerId: "off_…", // catalog offer id (see ovra.agenticCommerce.search)
50
- merchant: "Notion",
51
- amount: 79.0, // €79.00
52
- purpose: "Monthly Notion Team Plan renewal",
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",
53
22
  });
54
23
 
55
24
  if (result.status === "completed") {
56
- console.log(result.orderId, result.order.authorization.network_auth_code);
57
- } else {
58
- console.error("buy failed:", result.reason);
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_…
59
29
  }
60
30
  ```
61
31
 
62
- Prefer the raw operation? Same call, different envelope shape:
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.
63
35
 
64
- ```ts
65
- const { data: order } = await ovra.agenticCommerce.buy({
66
- body: {
67
- agent_id: "agt_…",
68
- offer_id: "off_…",
69
- purpose: "Monthly Notion Team Plan renewal",
70
- },
71
- });
72
- console.log(order.order_id, order.status);
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
+ ---
55
+
56
+ ## Install
57
+
58
+ ```sh
59
+ npm i @ovra/ts-sdk # or pnpm add / yarn add / bun add
73
60
  ```
74
61
 
75
- Need the lower-level intent / checkout pipeline (custom approval UI,
76
- out-of-band capture)? The primitives are still here:
62
+ Get a `sk_test_*` key from `app.getovra.com` Settings API keys.
63
+
64
+ ---
65
+
66
+ ## 60-second tour
67
+
68
+ ### The one-call AP2 purchase (recommended)
77
69
 
78
70
  ```ts
79
- // 1. Provision an agent. `policy` is required — fetch one via
80
- // `ovra.policies.list()`. `profile.purpose` is required.
81
- const { data: agent } = await ovra.agents.create({
82
- body: {
83
- name: "Notion Buyer",
84
- policy: "po_…",
85
- profile: { purpose: "Monthly Notion Team Plan renewal" },
86
- },
87
- });
71
+ import { Ovra } from "@ovra/ts-sdk";
88
72
 
89
- // 2. Declare a spending intent (`agent`, not `agent_id`).
90
- const { data: intent } = await ovra.intents.create({
91
- body: {
92
- agent: agent.id,
93
- merchant: "Notion",
94
- amount: { amount: 7900, currency: "eur" }, // €79.00
95
- purpose: "Monthly Notion Team Plan renewal",
96
- },
97
- });
73
+ const ovra = new Ovra({ apiKey: process.env.OVRA_API_KEY! });
98
74
 
99
- // 3. Approve the intent (production: this comes from the human
100
- // approving in the dashboard or a passkey assertion).
101
- await ovra.intents.approve({ path: { id: intent.id } });
75
+ // 1. Discover an offer
76
+ const { data: offers } = await ovra.agenticCommerce.search({
77
+ body: { vertical: "ai-credits", query: { provider: "openai", amount_usd: 10 } },
78
+ });
102
79
 
103
- // 4. Execute the checkout `intent` + `target_url` (the merchant URL
104
- // the DPAN will be submitted to) are both required.
105
- const { data: result } = await ovra.checkout.execute({
106
- body: {
107
- intent: intent.id,
108
- target_url: "https://www.notion.so/checkout",
109
- },
80
+ // 2. Buy itone 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,
110
86
  });
111
87
 
112
- console.log(result.transaction_id, result.status);
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 }
113
92
  ```
114
93
 
115
- Amounts on the wire are integers in the currency's minor units
116
- (`amount: 7900` = €79.00). Card PAN / CVV are **never** returned — only
117
- `last4` and a tokenized DPAN reach your code.
94
+ ### Reading + reconciling
118
95
 
119
- ## Authentication
96
+ ```ts
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);
120
100
 
121
- The constructor accepts any Ovra credential prefix. Pick the smallest scope
122
- that fits the call site.
101
+ // Get one with the full UCP envelope (line_items + fulfillment + adjustments)
102
+ const detail = await ovra.agenticCommerce.getOrder({ path: { order_id: "cmo_…" } });
123
103
 
124
- | Prefix | What it is | Use when |
125
- | ------------------- | ---------------------- | ------------------------------------------------------------------- |
126
- | `sk_live_…` | Full workspace key | Server-side production code that needs to mint/rotate/manage. |
127
- | `sk_test_…` | Full key, sandbox mode | Local dev and CI; `livemode: false` on every response. |
128
- | `rk_…` / `rk_test_…` | Restricted key | Read + scoped writes; blocked from key, webhook, billing, policy mutations. Safe to embed in less-trusted runtimes. |
129
- | `at_…` | Agent token | Scoped to one agent with a hard spend cap. The credential you ship to an agent runtime. |
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
+ ```
108
+
109
+ ### Refunds
130
110
 
131
111
  ```ts
132
- const ovra = new Ovra({
133
- apiKey: process.env.OVRA_API_KEY!,
134
- // Sent as the `X-Ovra-Client-App` header on every request — used for
135
- // analytics and to help support trace issues back to your integration.
136
- appInfo: { name: "AcmeCRM", version: "1.4.2" },
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" },
137
116
  });
138
117
 
139
- // Best-effort scope hint derived from the key prefix useful for
140
- // dev-time logging only; the server is the source of truth.
141
- // sk_* → "full" | rk_* → "restricted" | at_* → "agent"
142
- console.log(ovra.keyScope);
118
+ // Or full refund (no body) order goes COMPLETED REFUNDED
119
+ await ovra.agenticCommerce.refundOrder({ path: { order_id: "cmo_…" } });
143
120
  ```
144
121
 
145
- Test-mode keys produce objects with `livemode: false`; live keys produce
146
- `livemode: true`. Whenever the API is deployed against the sandbox card
147
- issuer, **every** response is `livemode: false` regardless of key — the
148
- whole workspace is sandbox until the real issuer ships.
122
+ ---
149
123
 
150
- ## Money
124
+ ## Auth scopes
151
125
 
152
- Money is `{ amount, currency }` everywhere on the wire. `amount` is an
153
- integer in minor units; `currency` is a three-letter ISO code lowercased.
126
+ The constructor takes any Ovra credential prefix. Pick the smallest scope that fits.
154
127
 
155
- | Display | Wire value |
156
- | ------------ | ----------------------------------------- |
157
- | €50.00 | `{ amount: 5000, currency: "eur" }` |
158
- | €0.99 | `{ amount: 99, currency: "eur" }` |
159
- | £1,234.56 | `{ amount: 123456, currency: "gbp" }` |
160
- | ¥1,000 (JPY) | `{ amount: 1000, currency: "jpy" }` (JPY has no minor units; treat the integer as the whole-yen amount.) |
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. |
161
134
 
162
- Floats and decimal strings are rejected at the validation layer. There is
163
- no implicit unit conversion — what you send is what gets compared against
164
- the policy.
135
+ ```ts
136
+ const ovra = new Ovra({
137
+ apiKey: process.env.OVRA_API_KEY!,
138
+ appInfo: { name: "AcmeCRM", version: "1.4.2" }, // X-Ovra-Client-App header
139
+ });
165
140
 
166
- ## Timestamps
141
+ console.log(ovra.keyScope); // "full" | "restricted" | "agent" | "unknown" (dev-time hint)
142
+ ```
167
143
 
168
- Every timestamp is an ISO 8601 string in the `created` field
169
- (`"2026-05-25T18:04:16Z"`). The legacy `created_at` / `createdAt` / epoch
170
- millis variants are gone.
144
+ ---
171
145
 
172
146
  ## Errors
173
147
 
174
- API errors throw a typed subclass of `OvraError`. Branch on type, status,
175
- 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.
176
150
 
177
151
  ```ts
178
152
  import {
179
153
  Ovra,
180
154
  OvraError,
155
+ OvraAuthenticationError,
156
+ OvraPermissionError,
157
+ OvraPaymentRequiredError,
158
+ OvraConflictError,
181
159
  OvraNotFoundError,
182
- OvraRateLimitError,
183
160
  } from "@ovra/ts-sdk";
184
161
 
185
162
  try {
186
- await ovra.cards.get({ path: { id: "crd_…" } });
187
- } catch (err) {
188
- if (err instanceof OvraNotFoundError) {
189
- // 404 — card doesn't exist or isn't yours
190
- } else if (err instanceof OvraRateLimitError) {
191
- // 429 — back off and retry
192
- } else if (err instanceof OvraError && err.code === "E_POLICY_DENIED") {
193
- // policy gate refused the spend; `err.message` is the human string,
194
- // `err.param` points at the offending body field if any.
195
- } else {
196
- 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);
197
174
  }
198
175
  }
199
176
  ```
200
177
 
201
- 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).
202
180
 
203
- ```json
204
- {
205
- "error": {
206
- "type": "policy_error",
207
- "code": "E_POLICY_DENIED",
208
- "message": "Spend exceeds the configured policy limit.",
209
- "param": "amount",
210
- "doc_url": "https://docs.getovra.com/errors/E_POLICY_DENIED",
211
- "request_id": "req_…",
212
- "status": 403
213
- }
214
- }
215
- ```
216
-
217
- The thrown `OvraError` flattens that envelope onto the instance —
218
- `err.type`, `err.code`, `err.message`, `err.param`, `err.docUrl`,
219
- `err.requestId`, `err.status`. Branch on `err.code` — it's typed,
220
- ~50 canonical values, stable across releases. Never parse `err.message`
221
- — it's a human string and may change.
181
+ ---
222
182
 
223
183
  ## Idempotency
224
184
 
225
- Mutating requests (`POST`, `PATCH`, `DELETE`) require an `Idempotency-Key`
226
- header. **The SDK auto-mints a fresh UUID per call**, so retries through
227
- the built-in retry policy are safe by default. Override per-call when you
228
- 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:
229
191
 
230
192
  ```ts
231
- await ovra.transfers.create({
232
- headers: { "Idempotency-Key": "transfer-2026-q2-invoice-4711" },
233
- body: {
234
- from_wallet: "wal_…",
235
- to_beneficiary: "ben_…",
236
- amount: { amount: 250000, currency: "eur" },
237
- },
193
+ await ovra.intents.create({
194
+ body: { agent: "agt_…", merchant: "Notion", amount: { amount: 7900, currency: "eur" }, purpose: "…" },
195
+ headers: { "Idempotency-Key": "your-stable-key" },
238
196
  });
239
197
  ```
240
198
 
241
- Re-sending the same key + identical body returns the original response.
242
- Re-sending the same key with a *different* body returns
243
- `E_IDEMPOTENCY_CONFLICT` (HTTP 409). An in-flight collision returns
244
- `E_IDEMPOTENCY_IN_FLIGHT`.
199
+ ---
245
200
 
246
201
  ## Pagination
247
202
 
248
- Every list method is cursor-paginated and returns the canonical envelope
249
- directly — the SDK auto-strips hey-api's outer `{data, request, response}`
250
- 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`:
251
205
 
252
206
  ```ts
253
- const page1 = await ovra.cards.list({ query: { limit: 50 } });
254
- // ^? { 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
+ ```
255
214
 
256
- for (const card of page1.data) {
257
- console.log(card.id, card.last4);
258
- }
215
+ Default `limit` is 50, max 100. Bad cursors return `400 E_VALIDATION` —
216
+ no silent full-list fallback.
259
217
 
260
- if (page1.has_more) {
261
- const page2 = await ovra.cards.list({
262
- query: { limit: 50, starting_after: page1.next_cursor! },
263
- });
264
- }
265
- ```
218
+ ---
266
219
 
267
- 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 }`.
268
223
 
269
224
  ```ts
270
- import { paginate } from "@ovra/ts-sdk";
225
+ import { parseMoney, formatMoney } from "@ovra/ts-sdk";
271
226
 
272
- for await (const card of paginate(
273
- (args) => ovra.cards.list({ query: args }),
274
- { limit: 50 },
275
- )) {
276
- console.log(card.id, card.last4);
277
- }
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 €"
278
230
  ```
279
231
 
280
- 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.
281
235
 
282
- ```json
283
- {
284
- "object": "list",
285
- "data": [ /* … */ ],
286
- "has_more": true,
287
- "next_cursor": "card_01HE…",
288
- "url": "/v1/cards"
289
- }
290
- ```
236
+ ---
291
237
 
292
- ## Retries
238
+ ## Resource matrix
293
239
 
294
- Connection errors and 5xx / 429 / 408 responses retry automatically with
295
- jittered exponential backoff (500 ms → 4 s, 30 s wall-clock budget).
296
- Tune or disable per-client:
240
+ 33 typed namespaces. Scope + plan gating shown.
297
241
 
298
- ```ts
299
- const ovra = new Ovra({
300
- apiKey: "…",
301
- retry: { maxRetries: 5, maxElapsedMs: 60_000 },
302
- // or: retry: false
303
- });
304
- ```
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 |
305
279
 
306
- ## Agentic Commerce surface
280
+ </details>
307
281
 
308
- The AP2 sandbox is the canonical buy pipeline as of 0.4.0. Catalog
309
- discovery, the buy orchestrator, and the UCP-shaped order envelope
310
- (line_items / fulfillment / adjustments / totals / permalink_url) all
311
- live under `ovra.agenticCommerce`:
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>
293
+
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 |
299
+
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>
312
311
 
313
312
  ```ts
314
- // Browse the 10-vertical catalog
315
- const offers = await ovra.agenticCommerce.search({
316
- 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
+ },
317
320
  });
318
321
 
319
- // One-call buy mints DPAN + signs the 4-mandate AP2 chain + captures.
320
- // The route reads `agent_id`, `offer_id`, `purpose`, optional `card_id`
321
- // + `intent_id`. Amount + merchant are derived from the offer.
322
- 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({
323
324
  body: {
324
- agent_id: "agt_…",
325
- offer_id: "off_…",
325
+ agent: agent.id,
326
+ merchant: "Notion",
327
+ amount: { amount: 7900, currency: "eur" },
326
328
  purpose: "Monthly Notion Team Plan renewal",
327
329
  },
328
330
  });
329
331
 
330
- // Re-verify the AP2 chain against the cached mandate set
331
- const check = await ovra.agenticCommerce.verifyOrder({
332
- path: { order_id: order.data.order_id },
333
- });
332
+ // 3. Approve (production: passkey ceremony; sandbox: viaFixture=true)
333
+ await ovra.intents.approve({ path: { id: intent.id }, body: { confirm: true } });
334
334
 
335
- // UCP-conformant refundappend to adjustments, debit totals, flip
336
- // status to REFUNDED (full) or COMPENSATED (partial)
337
- await ovra.agenticCommerce.refundOrder({
338
- path: { order_id: order.data.order_id },
339
- 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" },
340
338
  });
339
+ console.log(result.transaction_id, result.status);
341
340
  ```
342
341
 
343
- ## Resource coverage
344
-
345
- `Ovra` binds every `/v1/*` resource as an ergonomic namespace —
346
- 33 namespaces, 141 operations total, all of them auto-typed from the
347
- generated client. Listed by domain:
348
-
349
- ### Money
350
-
351
- - `wallets` — `list`, `create`, `get`, `update`, `delete`
352
- - `transfers` — `list`, `create`, `get`
353
- - `beneficiaries` — `list`, `create`, `get`, `update`, `delete`
354
- - `refunds` — `list`, `create`, `get`
355
- - `paymentRequests` — `list`, `create`, `get`, `cancel`
356
- - `ledgerEntries` — `list`, `get` (double-entry ledger, read-only)
357
- - `billing` — `getSubscription`, `createSubscription`,
358
- `createPortalSession`, `getPaymentMethod`, `setupPaymentMethod`,
359
- `fundWallet`, `getSepaInstructions`, `getBalance`, `collectOverage`
360
- - `invoices` — `list`, `get`
361
-
362
- ### Agents & spending
363
-
364
- - `agents` — `list`, `create`, `get`, `update`, `delete`, `freeze`,
365
- `unfreeze`, `getByExternalId`
366
- - `cards` — `list`, `create`, `get`, `update`, `freeze`, `unfreeze`,
367
- `close`, `rotate`
368
- - `intents` — `list`, `create`, `get`, `approve`, `deny`, `cancel`,
369
- `issueCredentials`, `confirmPayment`
370
- - `transactions` — `list`, `get` (read-only)
371
- - `checkout` — `execute`, `confirm`
372
- - `agenticCommerce` — `listVerticals`, `search`, `getOffer`,
373
- `getMerchantJwk`, `buy`, `listOrders`, `getOrder`, `verifyOrder`,
374
- `refundOrder`
375
- - `runs` — `create`, `get`
376
- - `outcomes` — `list`, `create`, `get`
377
- - `forecast` — `get`
378
- - `pay()` — one-call buy convenience wrapper around
379
- `agenticCommerce.buy`
380
-
381
- ### Compliance & governance
382
-
383
- - `policies` — `list`, `create`, `get`, `update`, `delete`, `apply`
384
- - `approvalPolicies` — `create`, `get`, `update`, `delete`
385
- - `disputes` — `list`, `create`, `get`, `resolve`, `listEvidence`,
386
- `addEvidence` (nested per-dispute)
387
- - `disputeEvidence` — `list`, `create` (standalone file uploads)
388
- - `riskEvents` — `list`, `listViolations`, `listFraudAlerts`,
389
- `updateFraudAlert`, `getAgentRisk`, `unfreezeAgent`, `getConfig`,
390
- `updateConfig`, `getSummary`
391
- - `auditEvents` — `list`, `get`, `verifyChain` (tamper-evident)
392
- - `accessEvents` — `list`, `get`
393
- - `metrics` — `list`
394
-
395
- ### Identity & access
396
-
397
- - `customers` — `me`, `create`, `get`, `update`
398
- - `apiKeys` — `list`, `create`, `get`, `delete` (requires `sk_*` scope)
399
- - `delegations` — `list`, `create`, `get`, `delete` (mint `at_*` tokens)
400
- - `passkeys` — `list`, `revoke`
401
- - `merchantCategories` — `list`, `get` (MCC lookup)
402
-
403
- ### Webhooks & notifications
404
-
405
- - `webhooks` — `list`, `create`, `get`, `update`, `delete`,
406
- `rotateSecret`, `ping`, `listDeliveries`
407
- - `notifications` — `list`, `unreadCount`, `markRead`, `markAllRead`
408
- (user-scoped — token credential, not `sk_*`)
409
- - `pushSubscriptions` — `getVapidPublicKey`, `register`, `delete`
410
- (Web Push for the approve PWA)
411
-
412
- ### Scope requirements
413
-
414
- Most write namespaces require an `sk_*` workspace key. The following
415
- will 403 with a restricted (`rk_*`) or agent (`at_*`) token:
416
-
417
- - `apiKeys.*`, `delegations.create|delete`, `webhooks.*` mutations,
418
- `policies.*` mutations, `approvalPolicies.*`, `billing.*` mutations,
419
- `riskEvents.updateConfig|unfreezeAgent`, `customers.create|update`
420
-
421
- Reads on those surfaces are allowed with `rk_*`. `notifications`,
422
- `pushSubscriptions`, and `passkeys` are user-scoped — they want a
423
- session credential, not a workspace key.
424
-
425
- ### Plan requirements
426
-
427
- A handful of namespaces are gated on the workspace's billing plan and
428
- will throw `E_PLAN_UPGRADE_REQUIRED` (HTTP 402) or
429
- `E_PERMISSION_DENIED` (HTTP 403) on Starter:
430
-
431
- - **Business plan or higher** — `auditEvents.*`, `metrics.*`,
432
- `riskEvents.*`, `delegations.*`
433
-
434
- Catch the error and surface the upgrade prompt:
342
+ </details>
343
+
344
+ <details><summary><b>List + reconcile transactions for one agent</b></summary>
435
345
 
436
346
  ```ts
437
- try {
438
- await ovra.auditEvents.list();
439
- } catch (err) {
440
- if (err instanceof OvraError && err.code === "E_PLAN_UPGRADE_REQUIRED") {
441
- // route the user to getovra.com/dashboard/billing
442
- }
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
+ ```
361
+
362
+ </details>
363
+
364
+ <details><summary><b>Register a webhook + handle delivery</b></summary>
365
+
366
+ ```ts
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");
443
388
  }
444
389
  ```
445
390
 
446
- ### Tree-shaking
391
+ </details>
447
392
 
448
- Prefer raw functions over the class? Every operation is also exported
449
- from `@ovra/ts-sdk/api`:
393
+ <details><summary><b>Issue a delegation (sub-agent / B2B2B)</b></summary>
450
394
 
451
395
  ```ts
452
- import { listApiKeys } from "@ovra/ts-sdk/api";
453
- const keys = await listApiKeys({ query: { limit: 10 } });
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_*
454
405
  ```
455
406
 
456
- The full inventory is in `apps/api/openapi.json` (single source of
457
- truth — the SDK is regenerated from it on every release).
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
+ ---
458
430
 
459
431
  ## Publishing
460
432
 
461
- > **Status (2026-05-28):** auto-publish via
462
- > `.github/workflows/sdk-publish.yaml` is wired correctly but
463
- > **currently blocked at the runner-allocation step** — the GitHub
464
- > account has a billing issue ("recent account payments have failed
465
- > or your spending limit needs to be increased"). All workflow runs
466
- > exit in ~3-5 s before the YAML steps start. Until billing is
467
- > restored, every release ships via the manual script below; both
468
- > `0.4.0` and `0.4.1` were published this way.
433
+ <details><summary>How releases ship (npm + GitHub Actions)</summary>
469
434
 
470
- Manual release flow (single command runs regen + build + version
471
- check + `npm publish`):
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:
472
439
 
473
440
  ```sh
474
- # Bump packages/sdk/package.json version first (semver: patch / minor / major)
475
- pnpm --filter @ovra/ts-sdk version patch --no-git-tag-version
476
-
477
- # Then publish (refuses if local <= remote, or if git tree is dirty)
478
- pnpm run publish:sdk # or: pnpm --filter @ovra/ts-sdk run publish-manual
479
- pnpm run publish:sdk:dry # dry-run (no upload, full pipeline otherwise)
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
480
444
  ```
481
445
 
482
- The script lives at `scripts/publish-sdk.sh`. Requirements: logged in
483
- to npm as a publisher on the `@ovra` scope (`npm login`), clean git
484
- tree, local version strictly greater than the version currently on
485
- npm.
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>
486
455
 
487
- Once GH Actions billing is restored, every push to `main` that
488
- touches `apps/api/openapi.json`, `packages/sdk/src/**`,
489
- `packages/sdk/openapi-ts.config.ts`, or `packages/sdk/package.json`
490
- will regenerate, bump (patch), and publish automatically — the
491
- workflow is already in place.
456
+ ---
492
457
 
493
458
  ## License
494
459
 
495
- Commercialsee [LICENSE](./LICENSE). The Ovra service requires a paid
496
- 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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovra/ts-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Official TypeScript SDK for the Ovra API — payment infrastructure for AI agents.",
5
5
  "author": "Ovra",
6
6
  "license": "SEE LICENSE IN LICENSE",