# Loomal Documentation (Full)
> Complete documentation for Loomal — identity infrastructure for AI agents.
## Related Resources
- Website: https://loomal.ai
- Website LLM index: https://loomal.ai/llms.txt
- Website LLM full: https://loomal.ai/llms-full.txt
- Console: https://console.loomal.ai
- API: https://api.loomal.ai
- MCP: https://smithery.ai/servers/loomal/identity
---
# Loomal (/docs)
The payments layer for AI agents. You operate a paid HTTP endpoint or MCP tool; Loomal lets any paying agent (or human) call it — five lines of middleware, USDC settles on Base, signed receipt back.
New here? Start with [Introduction](/docs/introduction) for the vocabulary, then ship a paid endpoint below.
> Building the agent side — wallet, inbox, vault, calendar, DID? That moved to [Mailgent](https://docs.mailgent.dev).
***
Accept payments [#accept-payments]
You're shipping a paid API or MCP tool. The middleware handles the x402 dance: serve a 402 challenge, verify the agent's signed USDC authorization, settle on-chain, return an Ed25519 receipt.
**1. Create a project** at [console.loomal.ai](https://console.loomal.ai). Flip **Pay → Accept** on. Copy the API key (`loid-...`).
**2. Install and wrap a handler:**
```bash
npm install @loomal/sdk express
```
```ts title="server.ts"
import express from "express";
import { Loomal } from "@loomal/sdk";
import { requirePayment } from "@loomal/sdk/paywall/express";
const app = express();
app.set("trust proxy", true);
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
app.get(
"/search",
requirePayment({ amount: "0.05" }),
(req, res) => res.json({ results: [/* ... */] }),
);
app.listen(3030);
```
Run with `LOOMAL_API_KEY=loid-...`. That's the whole integration.
Same shape on Hono, FastAPI, and MCP — only the middleware import changes. For frameworks without shipped middleware (Fastify, NestJS, Next.js, raw Node), call `loomal.payments.challenge` and `loomal.payments.redeem` from your handler directly.
The fastest path: create a project, wrap a handler, take your first paid call.
The end-to-end flow: frameworks, ngrok testing, webhooks, refunds.
Express / Hono / Next.js variants side-by-side, ready to clone.
***
More [#more]
REST endpoints with request/response shapes. For when the SDK isn't enough.
Register a paid URL for per-endpoint webhooks, revenue counters, and named stats.
A complete paid search API in Express, Hono, and Next.js.
---
# Introduction (/docs/introduction)
AI agents need to **pay for things**, and the services they call need a way to **get paid**. Credit cards don't fit: agents have no credit score, and card networks take a 30¢ minimum on every charge. Loomal is the payments layer built for them — price any endpoint in USDC, settle on Base in seconds, return a signed receipt.
> Building the agent side — a wallet, inbox, vault, calendar, and DID for an agent identity? That moved to [Mailgent](https://docs.mailgent.dev). Loomal is the seller/payments product.
Earn USDC per call [#earn-usdc-per-call]
You operate a paid HTTP endpoint or MCP tool. Drop our middleware on a handler, and any paying agent (or human) can call it — the SDK handles the [x402](https://www.x402.org/) challenge, verifies the agent's signed USDC authorization, settles on Base, and returns a signed receipt.
```ts
app.get(
"/search",
requirePayment({ amount: "0.05" }),
(req, res) => res.json({ results: [/* ... */] }),
);
```
→ [Quickstart](/docs/get-started/quickstart) — ship a paid endpoint in 5 minutes.
How the pieces fit [#how-the-pieces-fit]
Every API key belongs to a **Project**. Every Project belongs to an **Org** (your workspace and billing container). Turn on **Pay → Accept** in a project, wrap a handler with `requirePayment`, and you're charging per call. That's the whole shape.
Where to go next [#where-to-go-next]
* **Shipping a paid endpoint?** → [Quickstart](/docs/get-started/quickstart)
* **Want the full flow** (frameworks, local testing, webhooks, refunds)? → [Accept Payments](/docs/for-seller/accept-payments)
---
# Paid Search API (/docs/examples/paid-api)
A complete HTTP endpoint that returns search results gated by a $0.05 USDC payment. The [accept-payments](/docs/for-seller/accept-payments) flow end to end: install the SDK, wrap a handler, ship. Pick the framework closest to your stack — same logic, only the middleware import changes.
Before you start [#before-you-start]
* A project at [console.loomal.ai](https://console.loomal.ai) with **Pay → Accept** turned on.
* Your `loid-...` API key from the project dashboard.
* An agent to call your endpoint. The reference caller lives in [`loomal-ai/loomal-pay-examples`](https://github.com/loomal-ai/loomal-pay-examples).
```bash
npm install @loomal/sdk express hono @hono/node-server next
```
Express [#express]
`requirePayment` is the whole integration. It runs the 402 challenge on the first hit, then verifies and settles the agent's signed authorization on the retry. Set `LOOMAL_API_KEY=loid-...` in the environment.
```ts title="express/server.ts"
import express from "express";
import { Loomal } from "@loomal/sdk";
import { requirePayment } from "@loomal/sdk/paywall/express";
const app = express();
app.set("trust proxy", true); // so resource URL is https:// behind a tunnel
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
app.get(
"/search",
requirePayment({ amount: "0.05", description: "Search API" }),
(req, res) => {
res.json({
query: req.query.q,
results: [{ title: "Result A", url: "https://example.com/a" }],
});
},
);
app.listen(3030, () => console.log("listening on :3030"));
```
Hono [#hono]
Works on Node, Bun, Deno, and Cloudflare Workers. The middleware signature is identical to Express — only the import path changes.
```ts title="hono/server.ts"
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { Loomal } from "@loomal/sdk";
import { requirePayment } from "@loomal/sdk/paywall/hono";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const app = new Hono();
app.get(
"/search",
requirePayment({ amount: "0.05", description: "Search API" }),
(c) =>
c.json({
query: c.req.query("q"),
results: [{ title: "Result A", url: "https://example.com/a" }],
}),
);
serve({ fetch: app.fetch, port: 3030 });
```
Next.js (App Router) [#nextjs-app-router]
Next.js doesn't have shipped middleware, so call `loomal.payments.challenge` and `loomal.payments.redeem` from the route handler. This is exactly what `requirePayment` does internally.
```ts title="app/api/search/route.ts"
import { NextResponse } from "next/server";
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
export async function GET(req: Request) {
const url = new URL(req.url);
const resource = `${url.origin}${url.pathname}`;
const xPayment = req.headers.get("x-payment");
// No payment yet — return a 402 challenge describing what to pay.
if (!xPayment) {
const challenge = await loomal.payments.challenge({
amount: "0.05",
resource,
description: "Search API",
});
return NextResponse.json(challenge, { status: 402 });
}
// Agent retried with X-Payment — verify the signed authorization and settle.
const result = await loomal.payments.redeem({
paymentHeader: xPayment,
resource,
amount: "0.05",
});
if (!result.ok) {
return NextResponse.json(result.requirement, { status: 402 });
}
return NextResponse.json(
{
query: url.searchParams.get("q"),
results: [{ title: "Result A", url: "https://example.com/a" }],
},
{ status: 200, headers: { "X-Payment-Response": result.paymentResponse } },
);
}
```
What the agent sees [#what-the-agent-sees]
```bash
# First request — no payment header. Server returns 402 with what to pay.
curl -i http://localhost:3030/search?q=test
# HTTP/1.1 402 Payment Required
# {"x402Version":1,"accepts":[{"scheme":"exact","network":"base", ...}]}
# Agent signs an EIP-3009 authorization and retries with X-Payment.
# On success, server returns 200 with the result and an Ed25519 receipt.
# HTTP/1.1 200 OK
# X-Payment-Response:
# {"query":"test","results":[...]}
```
The on-chain settle shows up in **console.loomal.ai → Pay → Recent payments** within a second.
Related [#related]
Setup, tunnels for local testing, webhooks, programmatic payment list.
Working caller and seller — clone, install, run.
---
# Accept payments (/docs/for-seller/accept-payments)
You ship a paid HTTP endpoint or MCP tool. Loomal handles the x402 dance: serves a 402 challenge, verifies the agent's signed USDC authorization, settles on Base, and signs an Ed25519 receipt. This page is the end-to-end seller flow.
Before you start [#before-you-start]
* A project at [console.loomal.ai](https://console.loomal.ai) with **Pay → Accept** turned on.
* Your `loid-...` API key from the project dashboard.
* The SDK: `npm install @loomal/sdk` (Node/Bun/Deno) or `pip install "loomal-sdk[fastapi]"` (Python).
The five-line paywall [#the-five-line-paywall]
```ts title="server.ts"
import express from "express";
import { Loomal } from "@loomal/sdk";
import { requirePayment } from "@loomal/sdk/paywall/express";
const app = express();
app.set("trust proxy", true);
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
app.get(
"/search",
requirePayment({ amount: "0.05" }),
(req, res) => res.json({ results: [/* ... */] }),
);
app.listen(3030);
```
That's the whole integration. Set `LOOMAL_API_KEY=loid-...` and run. The middleware reads the env var automatically; `requirePayment` does the 402 challenge and the verify-and-settle step.
Other frameworks [#other-frameworks]
Same shape, different import:
```ts title="Hono (Node, Bun, Deno, Cloudflare Workers)"
import { Hono } from "hono";
import { requirePayment } from "@loomal/sdk/paywall/hono";
const app = new Hono();
app.get("/search", requirePayment({ amount: "0.05" }), (c) => c.json({ results: [] }));
```
```ts title="MCP tool"
import { requirePayment } from "@loomal/sdk/paywall/mcp";
server.tool(
"search",
{ description: "Paid search" },
requirePayment({ amount: "0.05" }, async (args) => ({ results: [/* ... */] })),
);
```
```python title="FastAPI"
from fastapi import FastAPI, Depends
from loomal.paywall import require_payment
app = FastAPI()
@app.get("/search", dependencies=[Depends(require_payment(amount="0.05"))])
def search():
return {"results": [...]}
```
Without middleware (custom framework) [#without-middleware-custom-framework]
For Fastify, NestJS, Next.js, or any framework Loomal doesn't ship middleware for, wire the two SDK calls into your handler:
```ts title="server.ts"
app.get("/search", async (req, res) => {
const resource = `${req.protocol}://${req.get("host")}${req.path}`;
const xPayment = req.get("x-payment");
if (!xPayment) {
const challenge = await loomal.payments.challenge({ amount: "0.05", resource });
return res.status(402).json(challenge);
}
const result = await loomal.payments.redeem({
paymentHeader: xPayment, resource, amount: "0.05",
});
if (!result.ok) return res.status(402).json(result.requirement);
res.setHeader("X-Payment-Response", result.paymentResponse);
res.json({ results: [/* ... */] });
});
```
This is exactly what `requirePayment` does internally. Same SDK client, no extra dependencies, works on any HTTP server.
Test locally [#test-locally]
`api.loomal.ai` fetches your URL from the public internet to drive the payment side, so `localhost` won't work. Expose your server with a tunnel:
```bash
ngrok http 3030
# Forwarding https://abcd-1234.ngrok-free.app -> http://localhost:3030
```
Two things keep the URL the agent signs in sync:
* `app.set("trust proxy", true)` — Express reports `https://` (not `http://`) behind a tunnel.
* In **console.loomal.ai → your project → Pay → Add endpoint**, register the **ngrok URL** (exact match: host + path).
Sanity check:
```bash
curl -i https://abcd-1234.ngrok-free.app/search
# HTTP/1.1 402 Payment Required
# { "x402Version": 1, "accepts": [{ ... }] }
```
See payments programmatically [#see-payments-programmatically]
```ts
const recent = await loomal.payments.list({ limit: 20 });
const one = await loomal.payments.get("cmoh...");
console.log(one.signedReceipt); // Ed25519-signed proof of payment
```
Same data the Pay tab shows. Use for in-product receipts, reconciliation, or your own admin views.
Webhooks [#webhooks]
In **console.loomal.ai → your project → Pay → Endpoints**, edit the endpoint and add a `webhookUrl`. Loomal generates a `whsec_…` signing secret and shows it once — copy it then.
Loomal POSTs `payment.received` to your URL on every successful settle. Body is the Ed25519-signed receipt; `X-Loomal-Signature: sha256=` is HMAC-SHA256 over the raw body. Companion headers: `X-Loomal-Event`, `X-Loomal-Idempotency-Key` (de-dupe on this).
```ts
import express from "express";
import { verifyWebhook } from "@loomal/sdk/webhook";
app.post(
"/webhooks/loomal",
express.raw({ type: "application/json" }),
async (req, res) => {
const ok = await verifyWebhook(
req.body.toString(),
req.header("x-loomal-signature"),
process.env.LOOMAL_WEBHOOK_SECRET!,
);
if (!ok) return res.status(400).send("invalid signature");
// de-dupe on x-loomal-idempotency-key, then handle the receipt
res.sendStatus(200);
},
);
```
```python
from loomal.webhook import verify_webhook
@app.post("/webhooks/loomal")
async def webhook(request: Request):
raw = await request.body()
ok = verify_webhook(
raw, request.headers.get("x-loomal-signature"),
os.environ["LOOMAL_WEBHOOK_SECRET"],
)
if not ok:
raise HTTPException(400, "invalid signature")
# handle event ...
```
Retry policy on delivery: 5xx and network errors back off (1s · 4s · 16s · 64s · 300s). 4xx aborts retries.
Related [#related]
Express, Hono, FastAPI variants side-by-side.
Wire format for the redeem endpoint.
Webhooks, revenue counters, and named stats per endpoint.
---
# Shopify (/docs/for-seller/shopify)
**Coming soon.** A native Shopify app for Loomal payments is on the roadmap — one-click install, product/price mapping, and order sync. This page will cover it end to end.
Accept agent payments today [#accept-agent-payments-today]
You don't need the native app to charge agents. Put Loomal's paywall in front of any endpoint your Shopify storefront or headless backend already exposes — about five lines of SDK middleware:
→ [Accept payments](/docs/for-seller/accept-payments) — the universal seller flow (HTTP + MCP), the same x402 settlement the Shopify app will use under the hood.
Want early access? [#want-early-access]
Tell us about your store at [loomal.ai/contact](https://loomal.ai/contact), or watch this page — we publish integrations as they ship.
---
# WooCommerce (/docs/for-seller/woocommerce)
**Coming soon.** A WooCommerce plugin for Loomal payments is on the roadmap — install from WordPress, map products to x402 prices, and sync orders automatically. This page will cover it end to end.
Accept agent payments today [#accept-agent-payments-today]
You don't need the plugin to charge agents. Put Loomal's paywall in front of any endpoint your WooCommerce site or REST backend already exposes — about five lines of SDK middleware:
→ [Accept payments](/docs/for-seller/accept-payments) — the universal seller flow (HTTP + MCP), the same x402 settlement the WooCommerce plugin will use under the hood.
Want early access? [#want-early-access]
Tell us about your store at [loomal.ai/contact](https://loomal.ai/contact), or watch this page — we publish integrations as they ship.
---
# Quickstart (/docs/get-started/quickstart)
Wrap a handler so paying agents (or anyone with USDC on Base) can call it.
> Building the agent side — an agent that consumes paid APIs, with its own wallet, inbox, and vault? That's [Mailgent](https://docs.mailgent.dev).
1\. Create a project [#1-create-a-project]
[console.loomal.ai](https://console.loomal.ai) → Create a project. You get an API key (`loid-...`).
In the project's **Pay** tab, switch **Accept payments** on — it's off by default on a fresh project.
2\. Install the SDK and wrap your handler [#2-install-the-sdk-and-wrap-your-handler]
```bash
npm install @loomal/sdk express
```
```ts title="server.ts"
import express from "express";
import { requirePayment } from "@loomal/sdk/paywall/express";
const app = express();
app.set("trust proxy", true);
app.get(
"/search",
requirePayment({ amount: "0.05" }),
(req, res) => res.json({ results: [/* ... */] }),
);
app.listen(3030);
```
Set `LOOMAL_API_KEY=loid-...` and run the server. The middleware handles the 402 challenge, verifies the signed retry, settles on Base, and returns the `X-Payment-Response` header.
Other frameworks:
| Stack | Import |
| ---------------- | ----------------------------------- |
| Hono | `@loomal/sdk/paywall/hono` |
| MCP (per-tool) | `@loomal/sdk/paywall/mcp` |
| FastAPI (Python) | `pip install "loomal-sdk[fastapi]"` |
3\. Make a paid request [#3-make-a-paid-request]
You need a caller with a few cents of USDC on Base to call your endpoint. The simplest path is to run a working caller from [`loomal-ai/loomal-pay-examples`](https://github.com/loomal-ai/loomal-pay-examples) (clone, install, run).
When the call succeeds, your project's **Pay → Activity** tab shows the settled payment with an on-chain link.
4\. Where to go next [#4-where-to-go-next]
* **Full payment flow** (raw REST, webhooks, refunds): [Accept Payments](/docs/for-seller/accept-payments).
* **Working example to clone:** [Paid Search API](/docs/examples/paid-api).
---
# Authentication (/docs/sdk-reference/authentication)
All API requests require a Bearer token in the `Authorization` header.
Base URL [#base-url]
```
https://api.loomal.ai
```
Authentication [#authentication]
Include your API key in every request:
```bash
curl https://api.loomal.ai/v0/payments \
-H "Authorization: Bearer loid-your-api-key"
```
API Key Types [#api-key-types]
Loomal has two key types. They are deliberately non-overlapping — use the right one for the job.
| Prefix | Name | Acts **as** or **on** | Use for |
| ------- | ------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `loid-` | Project key | Acts **as** a single project | Anything the project does as itself: build payment challenges, verify and settle payments, register endpoints, read payment records |
| `lopk-` | Platform key | Acts **on** the platform | Manage projects at the org level: create, list, update scopes, rotate keys, delete |
A `loid-` key cannot manage other projects. A `lopk-` key cannot act as any project. If you need both capabilities, use both keys.
Both formats are `{prefix}-{64 hex characters}`. Keys are shown only once at creation — store them securely.
Error Responses [#error-responses]
Missing Authorization header [#missing-authorization-header]
```json
{
"error": "unauthorized",
"message": "Missing or invalid Authorization header",
"status": 401
}
```
Invalid API key format [#invalid-api-key-format]
```json
{
"error": "unauthorized",
"message": "Invalid API key format",
"status": 401
}
```
Keys must start with `loid-` or `lopk-`. If the format is correct but the key is wrong:
```json
{
"error": "unauthorized",
"message": "Invalid API key",
"status": 401
}
```
Insufficient scope [#insufficient-scope]
```json
{
"error": "forbidden",
"message": "Missing required scope: payments:accept",
"status": 403
}
```
Rate Limits [#rate-limits]
API requests are rate-limited per identity. Current limits:
| Limit | Value |
| ------------- | ----------------------- |
| API requests | 200/minute per identity |
| Auth attempts | 60/minute per IP |
Rate limit headers are included in responses:
* `X-RateLimit-Remaining`
* `X-RateLimit-Reset`
When rate limited:
```json
{
"error": "rate_limited",
"message": "Too many requests",
"status": 429
}
```
Platform Keys [#platform-keys]
Platform keys (`lopk-`) are created in the console under your organization settings and unlock the `/v0/platform` endpoints for creating, rotating, and revoking projects programmatically.
---
# Errors (/docs/sdk-reference/errors)
All errors follow a consistent format:
```json
{
"error": "error_code",
"message": "Human-readable description",
"status": 400
}
```
Error Codes [#error-codes]
| Status | Error Code | Description |
| ------ | ------------------- | ------------------------------------------------------ |
| `400` | `bad_request` | Missing or invalid request body |
| `401` | `unauthorized` | Missing, invalid, or expired API key |
| `403` | `forbidden` | API key lacks required scope |
| `404` | `not_found` | Resource not found or not accessible |
| `413` | `payload_too_large` | Request body exceeds 2MB limit |
| `429` | `rate_limited` | Too many requests (200/min per project, 60/min per IP) |
| `500` | `internal_error` | Server error |
Common Scenarios [#common-scenarios]
Missing required field [#missing-required-field]
```json
{
"error": "bad_request",
"message": "amount and resource are required",
"status": 400
}
```
Payment not found [#payment-not-found]
```json
{
"error": "not_found",
"message": "Payment not found",
"status": 404
}
```
This also occurs when trying to access a payment that belongs to a different project.
Scope violation [#scope-violation]
```json
{
"error": "forbidden",
"message": "Missing required scope: payments:accept",
"status": 403
}
```
Rate limited [#rate-limited]
```json
{
"error": "rate_limited",
"message": "Too many requests",
"status": 429
}
```
---
# Pagination (/docs/sdk-reference/pagination)
List endpoints return paginated results using cursor-based pagination.
Parameters [#parameters]
| Param | Type | Default | Max | Description |
| ----------- | -------- | ------- | --- | ----------------------------- |
| `limit` | `number` | 50 | 100 | Results per page |
| `pageToken` | `string` | — | — | Cursor from previous response |
Response Format [#response-format]
```json
{
"payments": [...],
"count": 50,
"nextPageToken": "2026-03-28T09:00:00.000Z"
}
```
If `nextPageToken` is present, there are more results. Pass it as `pageToken` in the next request.
Example: Paginating Through All Payments [#example-paginating-through-all-payments]
```bash
# First page
curl "https://api.loomal.ai/v0/payments?limit=10" \
-H "Authorization: Bearer loid-your-api-key"
# Next page (use nextPageToken from previous response)
curl "https://api.loomal.ai/v0/payments?limit=10&pageToken=2026-03-28T09:00:00.000Z" \
-H "Authorization: Bearer loid-your-api-key"
```
Ordering [#ordering]
* **Payments**: Newest first (`createdAt` descending)
Endpoint Support [#endpoint-support]
| Endpoint | Pagination | Notes |
| ------------------ | -------------------- | ------------------------ |
| `GET /v0/payments` | `limit`, `pageToken` | Cursor-based, default 50 |
---
# Challenge (/docs/sdk-reference/payments/challenge)
**Scope:** `payments:accept`
Returns the standard x402 402 body to send back to an agent that hasn't paid yet. Loomal resolves your `payTo` wallet from your Identity automatically.
Most sellers don't call this directly — the `requirePayment` middleware does it for you. See [Accept payments](/docs/for-seller/accept-payments).
Request [#request]
```ts
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const challenge = await loomal.payments.challenge({
amount: "0.05",
resource: "https://your-api.com/search",
description: "Search API",
network: "base",
});
```
```python
import os
from loomal import Loomal
loomal = Loomal(api_key=os.environ["LOOMAL_API_KEY"])
challenge = loomal.payments.challenge(
amount="0.05",
resource="https://your-api.com/search",
description="Search API",
network="base",
)
```
```bash
curl -X POST https://api.loomal.ai/v0/payments/challenge \
-H "Authorization: Bearer loid-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"amount": "0.05",
"network": "base",
"resource": "https://your-api.com/search",
"description": "Search API"
}'
```
Body [#body]
| Field | Type | Required | Description |
| ------------- | -------- | -------- | -------------------------------------------------------------------- |
| `amount` | `string` | Yes | Decimal USDC, up to 6 fractional digits. E.g. `"0.05"`, `"1.234567"` |
| `network` | `string` | Yes | `"base"` (Base mainnet — only supported network) |
| `resource` | `string` | No | Full URL of the resource being paid for. Defaults to empty. |
| `description` | `string` | No | Human-readable label shown in the 402 body, max 500 chars |
Response — 200 OK [#response--200-ok]
```json
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "50000",
"resource": "https://your-api.com/search",
"description": "Search API",
"mimeType": "",
"payTo": "0xYourIdentityWalletAddress",
"maxTimeoutSeconds": 60,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": { "name": "USD Coin", "version": "2" }
}]
}
```
This is the canonical x402 spec response. Send the entire body back to the agent with HTTP `402 Payment Required`.
| Field | Description |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `maxAmountRequired` | Amount in raw USDC units (6 decimals). `"50000"` = 0.05 USDC. |
| `payTo` | Your Identity's wallet address. |
| `asset` | USDC contract on the chosen network. |
| `extra.name` / `extra.version` | EIP-712 domain for the USDC contract. On Base mainnet: `"USD Coin"` / `"2"`. |
Errors [#errors]
| Status | `error` | Cause |
| ------ | ------------------ | -------------------------------------------- |
| 400 | `bad_request` | Invalid `amount` or `network` |
| 401 | `unauthorized` | Missing or invalid `Authorization` header |
| 403 | `forbidden` | API key lacks `payments:accept` scope |
| 409 | `no_wallet` | Identity has no wallet provisioned |
| 503 | `feature_disabled` | Payments not enabled on this Loomal instance |
Next [#next]
[`POST /v0/payments/redeem`](/docs/sdk-reference/payments/redeem) — verify and settle the agent's signed payment.
---
# Get Payment (/docs/sdk-reference/payments/get)
**Scope:** `payments:accept`
Returns a single payment row including the full Ed25519 receipt — the seller-issued proof that the payment happened.
Request [#request]
```ts
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const payment = await loomal.payments.get("cmoh...");
```
```python
import os
from loomal import Loomal
loomal = Loomal(api_key=os.environ["LOOMAL_API_KEY"])
payment = loomal.payments.get("cmoh...")
```
```bash
curl https://api.loomal.ai/v0/payments/cmoh... \
-H "Authorization: Bearer loid-your-api-key"
```
Response — 200 OK [#response--200-ok]
```json
{
"id": "cmoh…",
"endpointId": "se_abc123",
"network": "base",
"payerAddress": "0x",
"recipientAddress": "0x",
"amountUsdcRaw": "50000",
"authorizationNonce": "0x<32-byte-hex>",
"txHash": "0x",
"status": "settled",
"resourceUrl": "https://your-api.com/search",
"failureReason": null,
"createdAt": "2026-04-28T12:34:56.789Z",
"settledAt": "2026-04-28T12:34:57.123Z",
"signedReceipt": {
"body": {
"version": 1,
"paymentInId": "cmoh…",
"endpointId": "se_abc123",
"identityId": "id-…",
"payerAddress": "0x",
"recipientAddress": "0x",
"amountUsdcRaw": "50000",
"network": "base",
"txHash": "0x",
"timestamp": "2026-04-28T12:34:57.000Z"
},
"signature": "",
"publicKey": "z6Mk…",
"did": "did:web:loomal.ai:identities:id-…"
}
}
```
Verifying the receipt [#verifying-the-receipt]
The receipt's `signature` is over a canonical JSON serialization of `body`, signed with the Identity's Ed25519 DID key. Verify with:
```ts
import nacl from "tweetnacl"
import canonicalize from "canonicalize"
import { decodeBase58btc } from "multiformats/bases/base58"
// publicKey is multibase-encoded (z…). Strip prefix byte (0xed01 for Ed25519) before nacl.verify.
const pubBytes = decodeBase58btc(receipt.publicKey).slice(2)
const sigBytes = Buffer.from(receipt.signature, "base64")
const msgBytes = new TextEncoder().encode(canonicalize(receipt.body))
const ok = nacl.sign.detached.verify(msgBytes, sigBytes, pubBytes)
```
Errors [#errors]
| Status | Cause |
| ------ | ---------------------------------------------- |
| 401 | Missing or invalid `Authorization` |
| 403 | API key lacks `payments:accept` scope |
| 404 | No payment with that ID owned by this Identity |
---
# List Payments (/docs/sdk-reference/payments/list)
**Scope:** `payments:accept`
Returns recent payments received by this Identity, newest first.
Request [#request]
```ts
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const { payments, count } = await loomal.payments.list({ limit: 20 });
```
```python
import os
from loomal import Loomal
loomal = Loomal(api_key=os.environ["LOOMAL_API_KEY"])
result = loomal.payments.list(limit=20)
```
```bash
curl https://api.loomal.ai/v0/payments?limit=20 \
-H "Authorization: Bearer loid-your-api-key"
```
Query Parameters [#query-parameters]
| Param | Type | Default | Description |
| ------- | -------- | ------- | --------------------------------- |
| `limit` | `number` | `20` | Max rows to return, capped at 100 |
Response — 200 OK [#response--200-ok]
```json
{
"payments": [
{
"id": "cmoh…",
"endpointId": "se_abc123",
"endpoint": {
"id": "se_abc123",
"urlPattern": "https://your-api.com/search",
"priceUsdc": "0.05"
},
"network": "base",
"payerAddress": "0x",
"recipientAddress": "0x",
"amountUsdcRaw": "50000",
"txHash": "0x",
"status": "settled",
"resourceUrl": "https://your-api.com/search",
"failureReason": null,
"createdAt": "2026-04-28T12:34:56.789Z",
"settledAt": "2026-04-28T12:34:57.123Z"
}
],
"count": 1
}
```
| Field | Description |
| ------------------------- | ------------------------------------------------------------------------------------ |
| `endpointId` / `endpoint` | Null if the payment landed at a URL with no registered endpoint. |
| `amountUsdcRaw` | Amount in raw USDC units (6 decimals). Divide by 1,000,000 to get the decimal value. |
| `status` | `"settled"`, `"verified"`, `"failed"`, or `"unpaid_delivered"`. |
| `resourceUrl` | Origin + path of the paid request (query stripped). |
| `txHash` | `null` until on-chain settle confirms. |
Errors [#errors]
| Status | Cause |
| ------ | ------------------------------------- |
| 401 | Missing or invalid `Authorization` |
| 403 | API key lacks `payments:accept` scope |
Next [#next]
[`GET /v0/payments/:id`](/docs/sdk-reference/payments/get) — fetch a single payment with its full Ed25519 receipt.
---
# Redeem (/docs/sdk-reference/payments/redeem)
**Scope:** `payments:accept`
Verifies an EIP-3009 signature from the `X-Payment` header, settles the USDC transfer on-chain, records the payment to your dashboard, signs an Ed25519 receipt, and dispatches your webhook (if registered).
Most sellers don't call this directly — the `requirePayment` middleware does it for you. See [Accept payments](/docs/for-seller/accept-payments).
Request [#request]
```ts
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const result = await loomal.payments.redeem({
paymentHeader: req.header("x-payment")!,
resource: "https://your-api.com/search",
amount: "0.05",
network: "base",
});
```
```python
import os
from loomal import Loomal
loomal = Loomal(api_key=os.environ["LOOMAL_API_KEY"])
result = loomal.payments.redeem(
payment_header=request.headers["x-payment"],
resource="https://your-api.com/search",
amount="0.05",
network="base",
)
```
```bash
curl -X POST https://api.loomal.ai/v0/payments/redeem \
-H "Authorization: Bearer loid-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"paymentHeader": "",
"resource": "https://your-api.com/search",
"amount": "0.05",
"network": "base"
}'
```
Body [#body]
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ----------------------------------------------------------------------------------- |
| `paymentHeader` | `string` | Yes | The payer's `X-Payment` header (base64-encoded JSON) |
| `resource` | `string` | Yes | Full URL of the paid request — query strings are stripped before matching endpoints |
| `amount` | `string` | Yes | Must match the amount used in the original `/challenge` |
| `network` | `string` | Yes | Must match the network used in the original `/challenge` |
| `description` | `string` | No | Optional, must match the description used in `/challenge` |
The `amount`, `network`, `resource`, and `description` are used to **reconstruct the exact PaymentRequirements** the payer must have signed against. If they don't match, verify fails.
Response — 200, settled [#response--200-settled]
```json
{
"ok": true,
"paymentResponse": "",
"txHash": "0x",
"payer": "0x",
"paymentInId": "cmoh…",
"signedReceipt": {
"body": { /* canonical payment data */ },
"signature": "",
"publicKey": ""
},
"recordingFailed": false
}
```
Set `paymentResponse` as the `X-Payment-Response` header on your 200 response so the agent can verify settlement.
| Field | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `txHash` | On-chain settle transaction. View on [BaseScan](https://basescan.org). |
| `payer` | Payer's EOA address (the signer of the EIP-3009 authorization). |
| `paymentInId` | Internal ID of the dashboard row. Lookup with `GET /v0/payments/:id`. |
| `signedReceipt` | Ed25519 receipt signed by your Identity's DID key — the seller-issued proof of payment. |
| `recordingFailed` | `true` if the on-chain settle succeeded but Loomal couldn't record it server-side. The transfer still stands; check your activity log for any settle anomalies to reconcile. |
Response — 200, rejected [#response--200-rejected]
```json
{
"ok": false,
"stage": "verify",
"reason": "invalid_signature",
"requirement": { "x402Version": 1, "accepts": [ /* … */ ] }
}
```
`stage` tells you where it failed:
* `"verify"` — signature, `payTo`, amount, or auth-payload check failed before the on-chain submit
* `"settle"` — the on-chain submit was rejected (most often: replay of an already-used nonce)
Send the `requirement` body back to the agent with HTTP 402. They can sign a fresh authorization and retry.
Errors [#errors]
| Status | `error` | Cause |
| ------ | ------------------ | ------------------------------------------------------------ |
| 400 | `bad_request` | Invalid body, malformed `paymentHeader`, or `payTo` mismatch |
| 401 | `unauthorized` | Missing or invalid `Authorization` header |
| 403 | `forbidden` | API key lacks `payments:accept` scope |
| 409 | `no_wallet` | Identity has no wallet provisioned |
| 503 | `feature_disabled` | Payments not enabled on this Loomal instance |
What happens server-side [#what-happens-server-side]
1. Loomal validates the `paymentHeader` against the x402 spec.
2. The payer's signed authorization is checked against the project's wallet and the expected amount. Mismatches return `{ ok: false, stage: "verify" }`.
3. On valid input, Loomal verifies the signature and settles the USDC transfer on-chain. Failures here return `{ ok: false, stage: "settle" }`.
4. On success, Loomal signs an Ed25519 receipt and returns it. The endpoint's webhook (if configured) gets the same receipt; the project owner gets a plain-text email notification; the payment shows up in the project's payment log.
The receipt, webhook dispatch, log entry, and email are best-effort — if any fail, the payment is still settled on-chain and you still get back `ok: true`.
---
# Delete Endpoint (/docs/sdk-reference/sellers/delete)
**Scope:** `payments:accept`
Marks the endpoint inactive. Future payments at the URL still record (with `endpoint: null` on the payment row) but no webhooks fire and counters stop incrementing. Historical payment data is preserved.
Admin endpoint — currently only available via REST or the Console UI.
Request [#request]
```bash
curl -X DELETE https://api.loomal.ai/v0/sellers/endpoints/se_abc123 \
-H "Authorization: Bearer loid-your-api-key"
```
Response — 204 No Content [#response--204-no-content]
Empty body.
Errors [#errors]
| Status | Cause |
| ------ | ----------------------------------------------- |
| 401 | Missing or invalid `Authorization` |
| 403 | API key lacks `payments:accept` scope |
| 404 | No endpoint with that ID owned by this Identity |
Re-registering [#re-registering]
To re-activate the URL, call [`PUT /v0/sellers/endpoints`](/docs/sdk-reference/sellers/register) with the same `urlPattern`. A new endpoint row is created (existing inactive row stays as audit trail).
---
# List Endpoints (/docs/sdk-reference/sellers/list)
**Scope:** `payments:accept`
Returns all paid endpoints registered to this Identity, newest first.
Admin endpoint — currently only available via REST or the Console UI.
Request [#request]
```bash
curl https://api.loomal.ai/v0/sellers/endpoints \
-H "Authorization: Bearer loid-your-api-key"
```
Response — 200 OK [#response--200-ok]
```json
{
"endpoints": [
{
"endpointId": "se_abc123",
"urlPattern": "https://your-api.com/search",
"priceUsdc": "0.05",
"network": "base",
"webhookUrl": "https://your-api.com/webhooks/payment",
"active": true,
"callCount": 1432,
"revenueUsdcRaw": "71600000",
"createdAt": "2026-04-28T12:00:00Z"
}
],
"count": 1
}
```
| Field | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| `callCount` | Number of successful settled payments to this endpoint |
| `revenueUsdcRaw` | Lifetime revenue in raw USDC units. `"71600000"` = 71.60 USDC |
| `active` | `false` after `DELETE` (soft delete; payments stop firing webhooks but historical data is preserved) |
Errors [#errors]
| Status | Cause |
| ------ | ------------------------------------- |
| 401 | Missing or invalid `Authorization` |
| 403 | API key lacks `payments:accept` scope |
---
# Register Endpoint (/docs/sdk-reference/sellers/register)
**Scope:** `payments:accept`
Registers (or updates) a paid endpoint. Registration is **optional** — payments still record without it. What you get by registering:
* A webhook fired on every successful payment to that URL
* Per-endpoint call count + revenue counters in the dashboard
* An auditable price + description displayed alongside payments
Admin endpoint — currently only available via REST or the Console UI.
The easiest way to register an endpoint with a webhook secret is **console.loomal.ai → your project → Pay → Endpoints → Add endpoint**. Loomal generates a `whsec_…` HMAC secret and shows it once. Use this REST endpoint when you need to provision endpoints from automation.
Request [#request]
```bash
curl -X PUT https://api.loomal.ai/v0/sellers/endpoints \
-H "Authorization: Bearer loid-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"urlPattern": "https://your-api.com/search",
"priceUsdc": "0.05",
"network": "base",
"webhookUrl": "https://your-api.com/webhooks/payment",
"webhookSecret": "a-strong-shared-secret-16-chars+"
}'
```
Body [#body]
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `urlPattern` | `string` | Yes | Full HTTPS URL of the paid endpoint. Must be public (HTTPS, no private IPs). |
| `priceUsdc` | `string` | Yes | Decimal USDC, up to 6 fractional digits |
| `network` | `string` | No | `"base"` (Base mainnet, default and only supported value) |
| `webhookUrl` | `string` | No | HTTPS URL — Loomal POSTs payment receipts here. SSRF-validated against private IPs at registration AND each delivery. |
| `webhookSecret` | `string` | No | Min 16 chars. Used to HMAC-sign webhook bodies. Stored encrypted. Omit and use the Console UI for a Loomal-generated secret. |
Response — 201 Created [#response--201-created]
```json
{
"endpointId": "se_abc123",
"urlPattern": "https://your-api.com/search",
"priceUsdc": "0.05",
"network": "base",
"webhookUrl": "https://your-api.com/webhooks/payment",
"active": true,
"createdAt": "2026-04-28T12:00:00Z"
}
```
Webhook payload [#webhook-payload]
When a payment settles, Loomal POSTs to your `webhookUrl`:
```http
POST /webhooks/payment HTTP/1.1
Content-Type: application/json
X-Loomal-Event: payment.received
X-Loomal-Signature: sha256=
X-Loomal-Idempotency-Key:
{
"type": "payment.received",
"data": {
"body": { /* canonical receipt body — see Get Payment */ },
"signature": "",
"publicKeyHex": "<64-char hex>",
"alg": "Ed25519"
},
"idempotencyKey": ""
}
```
Verify the signature with the SDK helper:
```ts
import { verifyWebhook } from "@loomal/sdk/webhook";
const ok = await verifyWebhook(
rawBody,
req.header("x-loomal-signature"),
process.env.LOOMAL_WEBHOOK_SECRET!,
);
if (!ok) return res.status(400).send("invalid signature");
```
```python
from loomal.webhook import verify_webhook
ok = verify_webhook(
raw_body,
request.headers.get("x-loomal-signature"),
os.environ["LOOMAL_WEBHOOK_SECRET"],
)
```
Loomal retries failed deliveries with backoff (1s, 4s, 16s, 64s, 300s) up to 5 times. 4xx responses abort the chain. De-dupe on `X-Loomal-Idempotency-Key`.
Errors [#errors]
| Status | `error` | Cause |
| ------ | ------------------ | ---------------------------------------------------------------- |
| 400 | `bad_request` | `urlPattern` missing/invalid, `priceUsdc` malformed or `<= 0` |
| 400 | `bad_request` | `webhookUrl` rejected — must be HTTPS and resolve to a public IP |
| 401 | `unauthorized` | Missing or invalid `Authorization` |
| 403 | `forbidden` | API key lacks `payments:accept` scope |
| 409 | `conflict` | Endpoint already exists for this Identity + URL combination |
| 503 | `feature_disabled` | Payments not enabled on this Loomal instance |
Next [#next]
[`GET /v0/sellers/endpoints`](/docs/sdk-reference/sellers/list) — list registered endpoints with their counters.