Register Endpoint
Register a paid URL with optional webhook
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
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
| 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
{
"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
When a payment settles, Loomal POSTs to your webhookUrl:
POST /webhooks/payment HTTP/1.1
Content-Type: application/json
X-Loomal-Event: payment.received
X-Loomal-Signature: sha256=<hex>
X-Loomal-Idempotency-Key: <uuid>
{
"type": "payment.received",
"data": {
"body": { /* canonical receipt body — see Get Payment */ },
"signature": "<base64 Ed25519>",
"publicKeyHex": "<64-char hex>",
"alg": "Ed25519"
},
"idempotencyKey": "<uuid>"
}Verify the signature with the SDK helper:
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");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
| 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
GET /v0/sellers/endpoints — list registered endpoints with their counters.