@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 +349 -269
- package/dist/client.d.ts +1790 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +159 -0
- package/dist/client.js.map +1 -1
- package/dist/generated/index.d.ts +2 -2
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/sdk.gen.d.ts +151 -1
- package/dist/generated/sdk.gen.d.ts.map +1 -1
- package/dist/generated/sdk.gen.js +286 -0
- package/dist/generated/sdk.gen.js.map +1 -1
- package/dist/generated/types.gen.d.ts +2128 -0
- package/dist/generated/types.gen.d.ts.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,381 +1,461 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@ovra/ts-sdk`
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@ovra/ts-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/@ovra/ts-sdk)
|
|
5
|
+
[](https://bundlephobia.com/package/@ovra/ts-sdk)
|
|
6
|
+
[](https://www.npmjs.com/package/@ovra/ts-sdk)
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
59
|
+
npm i @ovra/ts-sdk # or pnpm add / yarn add / bun add
|
|
28
60
|
```
|
|
29
61
|
|
|
30
|
-
|
|
31
|
-
Cloudflare Workers, Vercel Edge).
|
|
62
|
+
Get a `sk_test_*` key from `app.getovra.com` → Settings → API keys.
|
|
32
63
|
|
|
33
|
-
|
|
64
|
+
---
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
out-of-band capture)? The primitives are still here:
|
|
94
|
+
### Reading + reconciling
|
|
76
95
|
|
|
77
96
|
```ts
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
const
|
|
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
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
// approving in the dashboard or a passkey assertion).
|
|
100
|
-
await ovra.intents.approve({ path: { id: intent.id } });
|
|
109
|
+
### Refunds
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
+
// Or full refund (no body) — order goes COMPLETED → REFUNDED
|
|
119
|
+
await ovra.agenticCommerce.refundOrder({ path: { order_id: "cmo_…" } });
|
|
112
120
|
```
|
|
113
121
|
|
|
114
|
-
|
|
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
|
-
##
|
|
124
|
+
## Auth scopes
|
|
119
125
|
|
|
120
|
-
The constructor
|
|
121
|
-
that fits the call site.
|
|
126
|
+
The constructor takes any Ovra credential prefix. Pick the smallest scope that fits.
|
|
122
127
|
|
|
123
|
-
| Prefix
|
|
124
|
-
|
|
|
125
|
-
| `sk_live_…`
|
|
126
|
-
| `sk_test_…`
|
|
127
|
-
| `rk_…`
|
|
128
|
-
| `at_…`
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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.
|
|
186
|
-
} catch (
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
215
|
+
Default `limit` is 50, max 100. Bad cursors return `400 E_VALIDATION` —
|
|
216
|
+
no silent full-list fallback.
|
|
258
217
|
|
|
259
|
-
|
|
260
|
-
const page2 = await ovra.cards.list({
|
|
261
|
-
query: { limit: 50, starting_after: page1.next_cursor! },
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
```
|
|
218
|
+
---
|
|
265
219
|
|
|
266
|
-
|
|
220
|
+
## Money
|
|
221
|
+
|
|
222
|
+
Wire amounts are integer cents inside a Money object: `{ amount, currency }`.
|
|
267
223
|
|
|
268
224
|
```ts
|
|
269
|
-
import {
|
|
225
|
+
import { parseMoney, formatMoney } from "@ovra/ts-sdk";
|
|
270
226
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
238
|
+
## Resource matrix
|
|
292
239
|
|
|
293
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
242
|
+
<details><summary><b>Money & 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 & 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 & 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 & 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 & notifications</b> (3 namespaces)</summary>
|
|
304
293
|
|
|
305
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
live
|
|
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
|
-
//
|
|
314
|
-
const
|
|
315
|
-
body: {
|
|
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
|
-
//
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
//
|
|
330
|
-
|
|
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
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
342
|
+
</details>
|
|
343
343
|
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
381
|
-
|
|
460
|
+
UNLICENSED — internal use only until the public release lands. See the
|
|
461
|
+
repo for the in-progress [terms](https://github.com/ovra/ovra/blob/main/LICENSE).
|