# Nova Public API — LLM reference

Compact, machine-readable digest of the Nova developer API. The human-friendly
page lives at <https://novadao.app/devs>. Both are generated from the same TypeScript
spec, so they stay in lockstep.

- Base URL: `https://novadao.app`
- Versioned prefix: `/api/v1/`
- All requests and responses are JSON.
- Send the API key as `Authorization: Bearer <key>`. Never `X-Api-Key`.
- Server-to-server only by default. Browser callers (requests with an `Origin` header) are blocked unless the user has registered the origin in the API key's allowed_domains list.

## Production API contract

Nova v1 is intentionally small:

- One base path: every authenticated route is under `/api/v1`.
- One auth scheme: `Authorization: Bearer <key>`.
- One SDK surface: public SDKs call `/api/v1/*` only. `/api/wallet/*` is for the signed-in Nova SPA and uses cookies plus CSRF.
- Successful responses return the resource directly; errors always use `{ "error": ... }`.
- Money-moving POSTs require `Idempotency-Key`; `X-Idempotency-Key` is also accepted.
- Webhook receivers must verify `Nova-Signature` against the raw request body.
- Minimal breakage rule: v1 keeps current field names and meanings; additions are additive.

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

```bash
npm install @novadao/sdk
```

Package page: <https://www.npmjs.com/package/@novadao/sdk>

```ts
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:

```ts
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 `NovaApiError` with `status`, `code`, `message`, and `requestId` for error envelopes.
- Key typed failures by both `status` and `code`; the same code can appear with different HTTP statuses.
- Retry only network errors, `408`, `429`, and `5xx`. Respect `Retry-After`.
- Never retry `POST /pix/charges` without 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.create` in the SDK, even though the raw API accepts it as optional.
- Accept snake_case from the wire, but expose camelCase TypeScript types.

## Authentication

```
Authorization: Bearer nv_live_8aXk3rT9...
```

The user creates an API key from the Nova app (Account → Developer) after
their account is approved by Nova ops. Approval is one-time per account; the
user can mint and revoke keys freely after that.

## Rate limits (per key)

- Tier 1 (default): 60 req/min, 10,000 req/day. POST /pix/charges sub-limit: 30/min.
- Tier 2 (on request): 600 req/min, 200,000 req/day. POST /pix/charges sub-limit: 300/min.

Every authenticated response carries:
```
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
X-RateLimit-Daily-Limit, X-RateLimit-Daily-Remaining, X-RateLimit-Daily-Reset
```

429 responses include `Retry-After` (seconds).

## Idempotency

Send a unique `Idempotency-Key` header (1..255 ASCII chars) on POSTs that
create real-world state. `X-Idempotency-Key` is accepted as an alias. Required on
`POST /api/v1/pix/charges`; optional on payment-links and webhooks. Cached
responses live 24h. 5xx responses are not cached.

- Replay (same key, same body): cached response with `X-Idempotent-Replay: true`.
- In progress (same key still running): `409 idempotency_key_in_progress`.
- Conflict (same key, different body): `409 idempotency_key_reused`.

## Error envelope

All errors share this shape. The `request_id` is unique and the only identifier you need to share with Nova support.

```json
{
  "error": {
    "code": "<error_code>",
    "message": "Human-readable explanation.",
    "request_id": "req_...",
    "docs_url": "https://novadao.app/devs/errors#<error_code>"
  }
}
```

| 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

### GET /api/v1/health

Liveness probe.

Auth: none.

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:
```json
{ "ok": true, "service": "nova-public-api", "server_time": "ISO8601" }
```

Example — Check the API is up:
```bash
curl https://novadao.app/api/v1/health
```
Returns:
```json
{
  "ok": true,
  "service": "nova-public-api",
  "server_time": "2026-04-25T12:34:56.000Z"
}
```

### GET /api/v1/ping

Authenticated round-trip.

Auth: Bearer (API key required, account must be approved).

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` (header, string, required) — Bearer <your-api-key>

Response shape:
```json
{ "ok": true, "key_id": number, "tier": "tier1" | "tier2", "allowed_domains": string[], "server_time": "ISO8601", "request_id": "req_..." }
```

Example — Verify your key:
```bash
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/ping
```
Returns:
```json
{
  "ok": true,
  "key_id": 42,
  "tier": "tier1",
  "allowed_domains": [],
  "server_time": "2026-04-25T12:34:56.000Z",
  "request_id": "req_8aXk..."
}
```

### POST /api/v1/pix/charges

Create a PIX charge (QR code).

Auth: Bearer (API key required, account must be approved).
Idempotency: `Idempotency-Key` required.
Rate limit: tier1: 30/min · tier2: 300/min.

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.

Parameters:
- `Authorization` (header, string, required) — Bearer <your-api-key>
- `Idempotency-Key` (header, string, required) — A unique value you generate per request (1..255 ASCII chars). `X-Idempotency-Key` is also accepted.
- `amount_in_cents` (body, integer, required) — Charge amount in cents (BRL). Minimum 100 (R$1.00).
- `depix_address` (body, string, required) — Confidential Liquid Network address (`lq1...`) that will receive the DePix once the charge settles.

Response shape:
```json
{ "id": "...", "status": "pending", "amount_in_cents": number, "qr_copy_paste": "...", "qr_image_url": "...", "blockchain_tx_id": null, "created_at": "ISO8601" }
```

Example — Create a R$50 charge:
```bash
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..."
  }'
```
Returns:
```json
{
  "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"
}
```

### GET /api/v1/pix/charges/:id

Fetch a PIX charge.

Auth: Bearer (API key required, account must be approved).

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` (path, string, required) — The charge id returned from POST /pix/charges.

Response shape:
```json
{ "id": "...", "status": "pending" | "paid" | "expired" | "failed", "amount_in_cents": number, "qr_copy_paste": "...", "qr_image_url": "...", "blockchain_tx_id": "..." | null, "created_at": "ISO8601" }
```

Example — Check whether a charge was paid:
```bash
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/pix/charges/ch_a8b3
```
Returns:
```json
{
  "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"
}
```

### POST /api/v1/payment-links

Create a reusable payment link.

Auth: Bearer (API key required, account must be approved).
Idempotency: `Idempotency-Key` optional.
Rate limit: tier1: 60/min.

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

Parameters:
- `name` (body, string, required) — 1..80 chars. Used to generate a slug.
- `mode` (body, "fixed" | "range" | "open", required) — Pricing model.
- `amount_brl` (body, number) — Required when mode is "fixed".
- `min_brl` (body, number) — Used when mode is "range" (required) or "open" (optional).
- `max_brl` (body, number) — Used when mode is "range" (required) or "open" (optional).
- `depix_address` (body, string, required) — Confidential Liquid Network address (`lq1...`) that receives DePix on each successful payment.
- `options` (body, object) — Optional: { ask_name?: bool, ask_email?: bool, thank_you_message?: string, sales_limit?: int }.

Response shape:
```json
{ "id": "lk_...", "name": "...", "mode": "fixed" | "range" | "open", "status": "active", "url": "https://...", "handle": "...", "slug": "...", "amount_brl": number | null, "min_brl": number | null, "max_brl": number | null, "depix_address": "...", "created_at": "ISO8601" }
```

Example — Sell a pizza for R$25:
```bash
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..."
  }'
```
Returns:
```json
{
  "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"
}
```

### GET /api/v1/payment-links/:id

Fetch a payment link.

Auth: Bearer (API key required, account must be approved).

Returns the current state of a payment link including its public URL.

Parameters:
- `id` (path, string, required) — The link id.

Response shape:
```json
{ "id": "lk_...", ... same shape as POST response, but "status" can also be "paused" }
```

Example — Fetch a link before showing it:
```bash
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp
```
Returns:
```json
{
  "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"
}
```

### DELETE /api/v1/payment-links/:id

Pause a payment link.

Auth: Bearer (API key required, account must be approved).

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` (path, string, required) — The link id.

Response shape:
```json
{ "id": "lk_...", "name": "...", "mode": "fixed" | "range" | "open", "status": "paused", "url": "https://...", "handle": "...", "slug": "...", "amount_brl": number | null, "min_brl": number | null, "max_brl": number | null, "depix_address": "...", "created_at": "ISO8601" }
```

Example — Pause a link:
```bash
curl -X DELETE https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp \
  -H "Authorization: Bearer $NOVA_API_KEY"
```
Returns:
```json
{
  "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"
}
```

### GET /api/v1/webhooks

List webhooks.

Auth: Bearer (API key required, account must be approved).

Returns every webhook subscriber on your account.

Response shape:
```json
{ "webhooks": [{ "id": number, "url": "...", "events": string[], "status": "active" | "disabled", "created_at": "ISO8601" }] }
```

Example — List configured webhooks:
```bash
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/webhooks
```
Returns:
```json
{
  "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"
    }
  ]
}
```

### POST /api/v1/webhooks

Subscribe to events.

Auth: Bearer (API key required, account must be approved).
Idempotency: `Idempotency-Key` optional.

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` (body, string (URL), required) — HTTPS endpoint that will receive POST deliveries.
- `events` (body, string[], 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:
```json
{ "id": number, "url": "...", "events": string[], "status": "active", "signing_secret": "<base64url, shown once>", "created_at": "ISO8601" }
```

Example — Subscribe to paid charges:
```bash
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"]
  }'
```
Returns:
```json
{
  "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"
}
```

### DELETE /api/v1/webhooks/:id

Unsubscribe.

Auth: Bearer (API key required, account must be approved).

Removes a webhook subscriber. Already-queued deliveries still attempt.

Parameters:
- `id` (path, integer, required) — Webhook id.

Response shape:
```json
{ "id": number, "deleted": true }
```

Example — Remove a webhook:
```bash
curl -X DELETE https://novadao.app/api/v1/webhooks/7 \
  -H "Authorization: Bearer $NOVA_API_KEY"
```
Returns:
```json
{
  "id": 7,
  "deleted": true
}
```

### POST /api/v1/webhooks/:id/test

Fire a test delivery.

Auth: Bearer (API key required, account must be approved).

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` (path, integer, required) — Webhook id.

Response shape:
```json
{ "delivery_id": "whd_...", "queued_at": "ISO8601" }
```

Example — Send a signed test event:
```bash
curl -X POST https://novadao.app/api/v1/webhooks/7/test \
  -H "Authorization: Bearer $NOVA_API_KEY"
```
Returns:
```json
{
  "delivery_id": "whd_abc123",
  "queued_at": "2026-04-25T12:34:56.000Z"
}
```

## Webhooks

The user creates webhook subscribers from `POST /api/v1/webhooks`. Nova posts JSON to the registered URL whenever a subscribed event fires.

### Event names and payloads

- `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 payload is wrapped:

```json
{
  "api_version": "2026-04-24",
  "event": "<event_name>",
  "created_at": "ISO8601",
  "data": { ... event-specific fields ... }
}
```

### Delivery headers

```
Nova-Signature: t=<unix_seconds>,v1=<sha256_hmac_hex>
Nova-Event: <event_name>
Nova-Delivery-Id: whd_...
Nova-Api-Version: 2026-04-24
Content-Type: application/json; charset=utf-8
```

### Signature verification

`v1` is `HMAC-SHA256(signing_secret, t + "." + raw_body)`. The
`signing_secret` is returned exactly once when the webhook is created.
Reject signatures whose `t` is more than 5 minutes from your server clock.
Use the raw request bytes — never re-serialize.

### Retry policy

15-second timeout. 2xx is a success. Any other response queues a retry. The
schedule is 1m → 5m → 30m → 2h → 6h → 24h (six attempts including the
first). After the last attempt the delivery is marked `failed` and the
account owner is notified in their inbox.

## Versioning

`/api/v1/` is a permanent contract. New fields and event types are
additive. Breaking changes ship at `/api/v2/` with at least 12 months of
overlap; v1 responses carry `Deprecation: true` and `Sunset: <RFC 9745>`
headers during sunset. Webhook payloads include `api_version` so
subscribers can branch on it.

## Bug bounty and responsible disclosure

Private reports go to `hello@novadao.app` with subject `[security] <short description>`. Do not open public issues or disclose details until Nova confirms disclosure is okay.

In scope: production Nova product and API surfaces under `https://novadao.app`, including `/app`, `/api/v1/*`, account/auth flows, payment links, webhook signing and delivery, and security-impacting website or documentation issues. Test only on accounts, keys, webhooks, payment links, and data that the researcher owns or is explicitly allowed to test.

Rules: include impact, affected URL or endpoint, reproduction steps, and proof-of-concept details. 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 following these rules is covered by safe harbor.

Out of scope: denial-of-service, load testing, automated brute force, social engineering, phishing, physical attacks, spam, third-party providers, partner systems, browser extensions, user devices, non-production environments unless explicitly authorized, known duplicates, scanner-only output without demonstrated impact, theoretical issues without a practical proof of concept, and outdated-browser-only issues.

Rewards: validated reports may be eligible for discretionary rewards based on severity, real-world impact, reproducibility, exploitability, report quality, and whether the issue was already known.

## Support

- Telegram: <https://t.me/novadao_supportbot>
- Include the `request_id` from the error envelope in any ticket.
- Human docs: <https://novadao.app/devs>
