@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.
- package/README.md +340 -375
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,496 +1,461 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@ovra/ts-sdk)
|
|
4
|
-
[](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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
npm
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Requirements: Node.js 20+, or any runtime with `fetch` (Bun, Deno,
|
|
32
|
-
Cloudflare Workers, Vercel Edge).
|
|
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)
|
|
33
7
|
|
|
34
|
-
|
|
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_…",
|
|
49
|
-
offerId: "
|
|
50
|
-
merchant: "
|
|
51
|
-
amount:
|
|
52
|
-
purpose: "
|
|
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
|
|
57
|
-
|
|
58
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
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,
|
|
110
86
|
});
|
|
111
87
|
|
|
112
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
124
|
+
## Auth scopes
|
|
151
125
|
|
|
152
|
-
|
|
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
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
141
|
+
console.log(ovra.keyScope); // "full" | "restricted" | "agent" | "unknown" (dev-time hint)
|
|
142
|
+
```
|
|
167
143
|
|
|
168
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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.
|
|
187
|
-
} catch (
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
215
|
+
Default `limit` is 50, max 100. Bad cursors return `400 E_VALIDATION` —
|
|
216
|
+
no silent full-list fallback.
|
|
259
217
|
|
|
260
|
-
|
|
261
|
-
const page2 = await ovra.cards.list({
|
|
262
|
-
query: { limit: 50, starting_after: page1.next_cursor! },
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
```
|
|
218
|
+
---
|
|
266
219
|
|
|
267
|
-
|
|
220
|
+
## Money
|
|
221
|
+
|
|
222
|
+
Wire amounts are integer cents inside a Money object: `{ amount, currency }`.
|
|
268
223
|
|
|
269
224
|
```ts
|
|
270
|
-
import {
|
|
225
|
+
import { parseMoney, formatMoney } from "@ovra/ts-sdk";
|
|
271
226
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
238
|
+
## Resource matrix
|
|
293
239
|
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 |
|
|
305
279
|
|
|
306
|
-
|
|
280
|
+
</details>
|
|
307
281
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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>
|
|
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
|
-
//
|
|
315
|
-
const
|
|
316
|
-
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
|
+
},
|
|
317
320
|
});
|
|
318
321
|
|
|
319
|
-
//
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
//
|
|
331
|
-
|
|
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
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
391
|
+
</details>
|
|
447
392
|
|
|
448
|
-
|
|
449
|
-
from `@ovra/ts-sdk/api`:
|
|
393
|
+
<details><summary><b>Issue a delegation (sub-agent / B2B2B)</b></summary>
|
|
450
394
|
|
|
451
395
|
```ts
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
#
|
|
475
|
-
pnpm
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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).
|