# 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.