Nova Developer API
Accept PIX, generate payment links, and receive webhook callbacks from your own app. The API is predictable: Bearer key in the header, JSON request in, JSON response out.
On this page
- API contract
- Quickstart
- SDK package
- Authentication
- Browser callers (CORS)
- Rate limits
- Idempotency
- Errors
- Endpoints
- System
- GET Liveness probe
- GET Authenticated round-trip
- PIX charges
- POST Create a PIX charge (QR code)
- GET Fetch a PIX charge
- Payment links
- POST Create a reusable payment link
- GET Fetch a payment link
- DELETE Pause a payment link
- Webhooks
- GET List webhooks
- POST Subscribe to events
- DELETE Unsubscribe
- POST Fire a test delivery
- Webhooks
- Versioning
- Bug bounty
- Support
- llms.md
Production API contract #
Nova v1 is intentionally small. Keep integrations centered on these rules:
- One base path: every authenticated route is under
/api/v1. - One auth scheme: send
Authorization: Bearer <key>. Do not send API keys from browser code. - One SDK surface: public SDKs must call
/api/v1/*only./api/wallet/*is for the signed-in Nova SPA and uses cookies plus CSRF. - Direct success bodies: successful responses return the resource itself. Errors always use the same
{ "error": ... }envelope. - Safe retries: money-moving POSTs require
Idempotency-Key.X-Idempotency-Keyis accepted for compatibility. - Signed webhooks: verify
Nova-Signaturewith the raw request body before you trust an event.
Get API access #
You need three things:
- A Nova account. Create one if you don’t have it yet.
- Approval to use the API. Open the wallet app, go to Account → Developer, fill out the request, and wait for approval (usually under a day).
- An API key. Once approved, the same screen lets you create one. Copy the secret immediately — we hash it and never show it again.
Test that everything is wired up:
curl -H "Authorization: Bearer $NOVA_API_KEY" https://novadao.app/api/v1/ping
You should get back a JSON object with your key id, your tier, and the server time. If you get a 401, double-check the key. If you get a 403 about a domain, see Browser callers (CORS).
Generate your first PIX charge for R$1 (use a real Liquid address you control):
curl -X POST https://novadao.app/api/v1/pix/charges \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount_in_cents": 100,
"depix_address": "lq1..."
}'
You’ll get back an id, a qr_copy_paste string (paste this into any Brazilian banking app), and a Nova-branded qr_image_url. Pay the QR to see the charge transition to paid.
SDK package #
The @novadao/sdk TypeScript package stays thin: one HTTP transport, typed resource groups, typed errors, idempotency as an explicit option, and webhook verification included. It does not hide retries for POSTs unless the caller supplied an idempotency key.
Install from npm:
npm install @novadao/sdk
Package page: @novadao/sdk on npm.
type NovaApiError = {
status: number;
code: string;
message: string;
requestId: string;
};
type PixCharge = {
id: string;
status: 'pending' | 'paid' | 'expired' | 'failed';
amountInCents: number | null;
qrCopyPaste: string | null;
qrImageUrl: string | null;
blockchainTxId: string | null;
createdAt: string;
};
type PaymentLink = {
id: string;
name: string;
mode: 'fixed' | 'range' | 'open';
status: 'active' | 'paused';
url: string;
handle: string;
slug: string;
amountBrl: number | null;
minBrl: number | null;
maxBrl: number | null;
depixAddress: string;
createdAt: string;
};
type WebhookEndpoint = {
id: number;
url: string;
events: string[];
status: 'active' | 'disabled';
createdAt: string;
};
type CreatedWebhookEndpoint = WebhookEndpoint & {
signingSecret: string;
};
type NovaClient = {
pix: {
createCharge(
input: { amountInCents: number; depixAddress: string },
options: { idempotencyKey: string },
): Promise<PixCharge>;
getCharge(id: string): Promise<PixCharge>;
};
paymentLinks: {
create(input: CreatePaymentLink, options?: { idempotencyKey?: string }): Promise<PaymentLink>;
get(id: string): Promise<PaymentLink>;
pause(id: string): Promise<PaymentLink>;
};
webhooks: {
list(): Promise<WebhookEndpoint[]>;
create(
input: CreateWebhookEndpoint,
options: { idempotencyKey: string },
): Promise<CreatedWebhookEndpoint>;
remove(id: number): Promise<{ id: number; deleted: true }>;
test(id: number): Promise<{ deliveryId: string; queuedAt: string }>;
verifySignature(
rawBody: string | Uint8Array,
header: string,
secret: string,
options?: { toleranceSeconds?: number },
): Promise<boolean>;
};
};
Recommended usage:
import { NovaClient, createIdempotencyKey } from '@novadao/sdk';
const nova = new NovaClient({
apiKey: process.env.NOVA_API_KEY!,
});
const charge = await nova.pix.createCharge(
{ amountInCents: 100, depixAddress: 'lq1...' },
{ idempotencyKey: createIdempotencyKey('pix') },
);
Implementation rules:
- Throw a
NovaApiErrorwithstatus,code,message, andrequestIdwhen the API returns an error envelope. - Key typed failures by both
statusandcode. The same code can appear with different HTTP statuses. - Retry only network errors,
408,429, and5xx. RespectRetry-After. - Never retry
POST /pix/chargeswithout an idempotency key. For the same logical request, retry with the same key; use a new key only for a new charge. - Require an idempotency key for
webhooks.createin the SDK, even though the raw API accepts it as optional. - Accept snake_case from the wire, but expose camelCase types in TypeScript.
Authentication #
An API key is a long secret string that proves a request came from you. Treat it like a password: never put it in code that runs in a browser, never commit it to GitHub, and rotate it if you think it leaked.
Send your key in the standard Bearer header:
Authorization: Bearer nv_live_8aXk3rT9...
X-Api-Key. Sending it returns 401 auth_use_bearer pointing back here.What does “approval” mean?
The API can do real things — generate PIX charges, create payment links — so we hand-approve every account before any keys can be created. The form in the wallet app asks what you’re building and how much volume you expect. Most requests are reviewed within a business day.
IP allowlist
If you set an IP allowlist on your developer account (also in Account → Developer), the API rejects any request whose source IP isn’t on the list. This is optional but strongly recommended for production servers.
Browser callers (CORS) #
By default the Nova API accepts server-to-server requests only. If you try to call /api/v1/* from a browser, the response is 403 browser_origin_disallowed.
This is intentional. An API key in a browser is an API key in every visitor’s devtools. If you absolutely need a browser caller (say, a hybrid integration where the same key signs server- and client-side calls), you can register specific allowed domains for that key. The dashboard shows a clear warning when you do.
Rate limits #
Two budgets, both per API key:
- Per minute — 60 (tier1, default) or 600 (tier2, on request).
- Per day — 10,000 (tier1) or 200,000 (tier2).
Specific endpoints have their own sub-limits — see each endpoint’s notes below. Every authenticated response carries:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 47 X-RateLimit-Reset: 1714000060 X-RateLimit-Daily-Limit: 10000 X-RateLimit-Daily-Remaining: 9842 X-RateLimit-Daily-Reset: 1714086400
On 429 rate_limited you also get Retry-After (seconds). Back off and retry — don’t loop instantly.
Idempotency #
Network blips happen. If your POST /pix/charges request times out, you don’t know whether the charge was created or not. Idempotency keys make retrying safe.
Send a unique Idempotency-Key header (1..255 ASCII characters, anything you generate) on every retryable POST. If the same key arrives twice with the same body, we return the cached response with X-Idempotent-Replay: true instead of creating a second charge. If the same key arrives with a different body, we return 409 idempotency_key_reused so you know something is off.
Cached responses live 24 hours. Server errors (5xx) are never cached, so you can always retry a transient failure with the same key.
- Required on
POST /pix/charges(real money). - Optional on
POST /payment-linksandPOST /webhooks. Send one if you want safe retries.
Errors #
All errors share one envelope:
{
"error": {
"code": "rate_limited",
"message": "You are sending requests too quickly. Slow down and try again.",
"request_id": "req_8aXk3rT9...",
"docs_url": "https://novadao.app/devs/errors#rate_limited"
}
}
The request_id is unique per request. Include it in any support ticket — it lets us find the matching server log instantly.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | auth_missing | No Authorization header was sent. |
| 401 | auth_use_bearer | You sent X-Api-Key. Use Authorization: Bearer <key> instead. |
| 401 | auth_invalid | API key is invalid, revoked, or not approved. |
| 403 | browser_origin_disallowed | Browser request whose Origin is not in the key’s allowed_domains. Server-to-server requests should not send Origin. |
| 403 | ip_not_allowlisted | Request IP is not in the account’s IP allowlist. |
| 429 | rate_limited | Too many requests. Read X-RateLimit-Reset and Retry-After. Daily budget exhaustion uses the same code. |
| 400 / 409 / 422 | invalid_payload | Body, path, or operation failed validation. SDKs should keep both HTTP status and error.code. |
| 400 | invalid_json | The request body is missing or is not valid JSON. |
| 413 | payload_too_large | The request body is larger than the endpoint accepts. |
| 400 | idempotency_key_required | POST /pix/charges requires Idempotency-Key on every request. |
| 409 | idempotency_key_in_progress | A request with this Idempotency-Key is still processing. Retry the same request shortly. |
| 409 | idempotency_key_reused | You sent the same Idempotency-Key with a different request body. Generate a new key for new requests. |
| 404 | not_found | The resource does not exist or is not yours. |
| 503 | payment_service_unavailable | The payment provider is temporarily unavailable. Retry the same logical request with the same Idempotency-Key. |
| 500 | internal_error | Something went wrong on our side. |
Endpoints #
Base URL: https://novadao.app. Every authenticated route is under /api/v1/.
Liveness probe.
No auth, no rate limit, no DB. Returns 200 as long as the API is running. Use it for status pages and uptime monitors.
Response shape
Check the API is up
cURL
curl https://novadao.app/api/v1/health
Node.js
const res = await fetch('https://novadao.app/api/v1/health');
const data = await res.json();
console.log(data); Python
import httpx
res = httpx.get('https://novadao.app/api/v1/health')
print(res.json()) Returns
{
"ok": true,
"service": "nova-public-api",
"server_time": "2026-04-25T12:34:56.000Z"
} Authenticated round-trip.
Confirms your API key works and shows what the server sees: your key id, tier, allowed domains, and server time. The dashboard’s "test connection" button calls this.
Parameters
- Authorization required
Bearer <your-api-key>
Response shape
Verify your key
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/ping
Node.js
const res = await fetch('https://novadao.app/api/v1/ping', {
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
console.log(await res.json()); Python
import os, httpx
res = httpx.get(
'https://novadao.app/api/v1/ping',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
print(res.json()) Returns
{
"ok": true,
"key_id": 42,
"tier": "tier1",
"allowed_domains": [],
"server_time": "2026-04-25T12:34:56.000Z",
"request_id": "req_8aXk..."
} Create a PIX charge (QR code).
Generates a PIX QR code your customer can pay. Pay attention to two headers: Authorization and Idempotency-Key. The Idempotency-Key is REQUIRED — it lets you safely retry a network error without double-charging.
Rate limit: tier1: 30/min · tier2: 300/min
Parameters
- Authorization required
Bearer <your-api-key>
- Idempotency-Key required
A unique value you generate per request (1..255 ASCII chars). `X-Idempotency-Key` is also accepted.
- amount_in_cents required
Charge amount in cents (BRL). Minimum 100 (R$1.00).
- depix_address required
Confidential Liquid Network address (`lq1...`) that will receive the DePix once the charge settles.
Response shape
Create a R$50 charge
cURL
curl -X POST https://novadao.app/api/v1/pix/charges \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Idempotency-Key: order-1234" \
-H "Content-Type: application/json" \
-d '{
"amount_in_cents": 5000,
"depix_address": "lq1..."
}' Node.js
import { randomUUID } from 'node:crypto';
const res = await fetch('https://novadao.app/api/v1/pix/charges', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
'Idempotency-Key': randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount_in_cents: 5000,
depix_address: 'lq1...',
}),
});
const charge = await res.json();
console.log(charge.id, charge.qr_copy_paste); Python
import os, uuid, httpx
res = httpx.post(
'https://novadao.app/api/v1/pix/charges',
headers={
'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}',
'Idempotency-Key': str(uuid.uuid4()),
},
json={
'amount_in_cents': 5000,
'depix_address': 'lq1...',
},
)
charge = res.json()
print(charge['id'], charge['qr_copy_paste']) Returns
{
"id": "ch_a8b3...",
"status": "pending",
"amount_in_cents": 5000,
"qr_copy_paste": "00020126580014BR.GOV.BCB.PIX...",
"qr_image_url": "https://novadao.app/api/v1/pix/charges/ch_a8b3.../qr.png",
"blockchain_tx_id": null,
"created_at": "2026-04-25T12:34:56.000Z"
} Fetch a PIX charge.
Returns the current status of a PIX charge. Subscribe to `pix.charge.received` when you need the bank-side payment confirmation, and `pix.charge.paid` when you need the settled Liquid transaction.
Parameters
- id required
The charge id returned from POST /pix/charges.
Response shape
Check whether a charge was paid
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/pix/charges/ch_a8b3
Node.js
const res = await fetch('https://novadao.app/api/v1/pix/charges/ch_a8b3', {
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const charge = await res.json();
console.log(charge.status); Python
import os, httpx
res = httpx.get(
'https://novadao.app/api/v1/pix/charges/ch_a8b3',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
charge = res.json()
print(charge['status']) Returns
{
"id": "ch_a8b3...",
"status": "paid",
"amount_in_cents": 5000,
"qr_copy_paste": "00020126580014BR.GOV.BCB.PIX...",
"qr_image_url": "https://novadao.app/api/v1/pix/charges/ch_a8b3.../qr.png",
"blockchain_tx_id": "7f4a...",
"created_at": "2026-04-25T12:34:56.000Z"
} Create a reusable payment link.
Generates a public checkout URL (`https://novadao.app/c/<handle>/<slug>`) that accepts payments. Three modes: `fixed` (one amount), `range` (min/max), `open` (any amount, optional bounds).
Rate limit: tier1: 60/min
Parameters
- name required
1..80 chars. Used to generate a slug.
- mode required
Pricing model.
- amount_brl
Required when mode is "fixed".
- min_brl
Used when mode is "range" (required) or "open" (optional).
- max_brl
Used when mode is "range" (required) or "open" (optional).
- depix_address required
Confidential Liquid Network address (`lq1...`) that receives DePix on each successful payment.
- options
Optional: { ask_name?: bool, ask_email?: bool, thank_you_message?: string, sales_limit?: int }.
Response shape
Sell a pizza for R$25
cURL
curl -X POST https://novadao.app/api/v1/payment-links \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pizza margherita",
"mode": "fixed",
"amount_brl": 25,
"depix_address": "lq1..."
}' Node.js
const res = await fetch('https://novadao.app/api/v1/payment-links', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Pizza margherita',
mode: 'fixed',
amount_brl: 25,
depix_address: 'lq1...',
}),
});
const link = await res.json();
console.log(link.url); Python
import os, httpx
res = httpx.post(
'https://novadao.app/api/v1/payment-links',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
json={
'name': 'Pizza margherita',
'mode': 'fixed',
'amount_brl': 25,
'depix_address': 'lq1...',
},
)
link = res.json()
print(link['url']) Returns
{
"id": "lk_HQKFuN22r39cc1Dp",
"name": "Pizza margherita",
"mode": "fixed",
"status": "active",
"url": "https://novadao.app/c/myshop/pizza-margherita",
"handle": "myshop",
"slug": "pizza-margherita",
"amount_brl": 25,
"min_brl": null,
"max_brl": null,
"depix_address": "lq1...",
"created_at": "2026-04-25T12:34:56.000Z"
} Fetch a payment link.
Returns the current state of a payment link including its public URL.
Parameters
- id required
The link id.
Response shape
Fetch a link before showing it
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp
Node.js
const res = await fetch('https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp', {
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const link = await res.json();
console.log(link.status, link.url); Python
import os, httpx
res = httpx.get(
'https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
link = res.json()
print(link['status'], link['url']) Returns
{
"id": "lk_HQKFuN22r39cc1Dp",
"name": "Pizza margherita",
"mode": "fixed",
"status": "active",
"url": "https://novadao.app/c/myshop/pizza-margherita",
"handle": "myshop",
"slug": "pizza-margherita",
"amount_brl": 25,
"min_brl": null,
"max_brl": null,
"depix_address": "lq1...",
"created_at": "2026-04-25T12:34:56.000Z"
} Pause a payment link.
Soft delete: pauses the link so it stops accepting new payments. Already-issued URLs return a "paused" page instead of 404 so customers mid-checkout see a clear message.
Parameters
- id required
The link id.
Response shape
Pause a link
cURL
curl -X DELETE https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp \ -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
const res = await fetch('https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const link = await res.json();
console.log(link.status); Python
import os, httpx
res = httpx.delete(
'https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
link = res.json()
print(link['status']) Returns
{
"id": "lk_HQKFuN22r39cc1Dp",
"name": "Pizza margherita",
"mode": "fixed",
"status": "paused",
"url": "https://novadao.app/c/myshop/pizza-margherita",
"handle": "myshop",
"slug": "pizza-margherita",
"amount_brl": 25,
"min_brl": null,
"max_brl": null,
"depix_address": "lq1...",
"created_at": "2026-04-25T12:34:56.000Z"
} List webhooks.
Returns every webhook subscriber on your account.
Response shape
List configured webhooks
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/webhooks
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks', {
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const data = await res.json();
console.log(data.webhooks.length); Python
import os, httpx
res = httpx.get(
'https://novadao.app/api/v1/webhooks',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
data = res.json()
print(len(data['webhooks'])) Returns
{
"webhooks": [
{
"id": 7,
"url": "https://yourshop.com/webhooks/nova",
"events": ["pix.charge.paid", "payment_link.paid"],
"status": "active",
"created_at": "2026-04-25T12:34:56.000Z"
}
]
} Subscribe to events.
Creates a webhook subscriber. The response includes a `signing_secret` shown only once — store it; you’ll need it to verify every delivery’s signature.
Parameters
- url required
HTTPS endpoint that will receive POST deliveries.
- events required
One or more from: pix.charge.created, pix.charge.received, pix.charge.paid, pix.charge.expired, pix.charge.failed, payment_link.paid.
Response shape
Subscribe to paid charges
cURL
curl -X POST https://novadao.app/api/v1/webhooks \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Idempotency-Key: webhook-paid-events-v1" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourshop.com/webhooks/nova",
"events": ["pix.charge.paid", "payment_link.paid"]
}' Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
'Idempotency-Key': 'webhook-paid-events-v1',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://yourshop.com/webhooks/nova',
events: ['pix.charge.paid', 'payment_link.paid'],
}),
});
const wh = await res.json();
console.log('Save this signing secret:', wh.signing_secret); Python
import os, httpx
res = httpx.post(
'https://novadao.app/api/v1/webhooks',
headers={
'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}',
'Idempotency-Key': 'webhook-paid-events-v1',
},
json={
'url': 'https://yourshop.com/webhooks/nova',
'events': ['pix.charge.paid', 'payment_link.paid'],
},
)
wh = res.json()
print('Save this signing secret:', wh['signing_secret']) Returns
{
"id": 7,
"url": "https://yourshop.com/webhooks/nova",
"events": ["pix.charge.paid", "payment_link.paid"],
"status": "active",
"signing_secret": "Rll0hXsdn2hzp1G6...",
"created_at": "2026-04-25T12:34:56.000Z"
} Unsubscribe.
Removes a webhook subscriber. Already-queued deliveries still attempt.
Parameters
- id required
Webhook id.
Response shape
Remove a webhook
cURL
curl -X DELETE https://novadao.app/api/v1/webhooks/7 \ -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks/7', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
console.log(await res.json()); Python
import os, httpx
res = httpx.delete(
'https://novadao.app/api/v1/webhooks/7',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
print(res.json()) Returns
{
"id": 7,
"deleted": true
} Fire a test delivery.
Queues a `webhook.test.ping` delivery to your webhook URL. Use it to confirm your endpoint receives and verifies signatures correctly. The delivery is signed identically to real events.
Parameters
- id required
Webhook id.
Response shape
Send a signed test event
cURL
curl -X POST https://novadao.app/api/v1/webhooks/7/test \ -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks/7/test', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const delivery = await res.json();
console.log(delivery.delivery_id); Python
import os, httpx
res = httpx.post(
'https://novadao.app/api/v1/webhooks/7/test',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
delivery = res.json()
print(delivery['delivery_id']) Returns
{
"delivery_id": "whd_abc123",
"queued_at": "2026-04-25T12:34:56.000Z"
} Webhooks: events #
Nova posts a JSON payload to your webhook URL whenever something happens to one of your charges or payment links. Subscribe to the events you care about when you create the webhook.
For D+2 PIX charges, use pix.charge.received to confirm the payer completed the bank-side PIX. A delayed payment arrives with status: "delayed", settlement_status: "delayed", delay_until, confirmed_at, and blockchain_tx_id: null. Use pix.charge.paid only for final Liquid settlement with a blockchain_tx_id, and keep listening to pix.charge.failed for refunded, canceled, or errored charges.
- pix.charge.created
A PIX charge was just created. Useful to track creation in your own database.
- pix.charge.received
A customer paid the PIX charge at the bank rail. D+2 charges can arrive with `status: "delayed"`, `settlement_status: "delayed"`, `delay_until`, `confirmed_at`, and `blockchain_tx_id: null`, then settle later via `pix.charge.paid`.
- pix.charge.paid
The PIX charge settled on Liquid and includes the settlement transaction id.
- pix.charge.expired
A PIX charge expired before being paid.
- pix.charge.failed
A PIX charge failed (cancelled, refunded, or rejected by the network). Keep listening to this event even after `pix.charge.received`.
- payment_link.paid
A payment-link checkout completed. Fires alongside `pix.charge.paid` when the charge originated from a link.
Every delivery includes these headers:
Nova-Signature: t=1714000000,v1=8a7c3b... Nova-Event: pix.charge.paid Nova-Delivery-Id: whd_abc123 Nova-Api-Version: 2026-04-24
Webhooks: verify the signature #
We send you a stamp; you check the stamp matches before trusting the message. Without verification, anyone who learns your URL could send you fake events. The Nova-Signature header has two parts: t (Unix timestamp in seconds) and v1 (HMAC-SHA256 hex). The signed string is t + "." + raw_body. Compute the HMAC with your signing_secret (returned once when you created the webhook) and compare.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyNovaWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || !v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
if (expected.length !== v1.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
} Python
import hmac, hashlib, time
def verify_nova_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in header.split(','))
try:
t = int(parts['t']); v1 = parts['v1']
except KeyError:
return False
if abs(int(time.time()) - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) PHP
function verify_nova_webhook(string $body, string $header, string $secret, int $tolerance = 300): bool {
parse_str(str_replace(',', '&', $header), $parts);
$t = (int) ($parts['t'] ?? 0); $v1 = $parts['v1'] ?? '';
if (!$t || !$v1) return false;
if (abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
return hash_equals($expected, $v1);
} Webhooks: retry policy #
If your endpoint returns 2xx within 15 seconds, the delivery is marked complete. Anything else is a failure. Failures retry six times with backoff:
After the last attempt, the delivery is marked failed. Your dashboard shows it under "Webhook deliveries" with a manual replay button. The Nova app also drops a notification in your inbox.
Versioning #
The current version is /api/v1/. We treat it as a permanent contract. New fields and new event types are additive and don’t require you to change anything. If we ever ship a breaking change, it lands at /api/v2/ and v1 stays alive for at least 12 months. During the sunset window every v1 response carries a Deprecation: true header and a Sunset date.
Every webhook payload also carries an api_version field so subscribers can branch on it without hard-coding dates.
Bug bounty and responsible disclosure #
We welcome security reports for Nova product and API surfaces. Report findings privately to hello@novadao.app with the subject [security] <short description>. Do not open public issues or publish details until Nova confirms that disclosure is okay.
In scope
- Production Nova surfaces under
https://novadao.app, including/app,/api/v1/*, account and authentication flows, payment links, webhook signing and delivery, and security-impacting website or documentation issues. - Reports tested only against accounts, keys, webhooks, payment links, and data that you own or are explicitly allowed to test.
Rules
- Include a clear description, impact, affected URL or endpoint, reproduction steps, and any proof-of-concept code needed to validate the issue.
- Keep testing minimal. Do not access, modify, delete, or exfiltrate other users’ data. If sensitive data appears, stop testing and report immediately.
- Good-faith research that follows these rules is covered by safe harbor: Nova will not pursue legal action for authorized testing under this program.
Out of scope
- Denial-of-service, load testing, automated brute force, social engineering, phishing, physical attacks, spam, or abuse campaigns.
- Third-party providers, partner systems, browser extensions, user devices, and non-production environments unless Nova explicitly authorizes them.
- Known duplicates, scanner-only output without demonstrated impact, theoretical issues without a practical proof of concept, and issues that only affect outdated browsers or platforms.
Rewards
Validated reports may be eligible for discretionary rewards. Reward decisions consider severity, real-world impact, reproducibility, exploitability, report quality, and whether the issue was already known.
Support
Two ways to reach us:
- Telegram support bot — fastest path, replies under an hour during business hours.
- Include the
request_idfrom the error envelope when you write in. We can trace every call instantly with it.
For LLM-assisted integrations, point your tool at /devs/llms.md — same content, machine-readable.