Nova
API v1 · Stable

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
  1. API contract
  2. Quickstart
  3. SDK package
  4. Authentication
  5. Browser callers (CORS)
  6. Rate limits
  7. Idempotency
  8. Errors
  9. Endpoints
    1. System
    2. GET Liveness probe
    3. GET Authenticated round-trip
    4. PIX charges
    5. POST Create a PIX charge (QR code)
    6. GET Fetch a PIX charge
    7. Payment links
    8. POST Create a reusable payment link
    9. GET Fetch a payment link
    10. DELETE Pause a payment link
    11. Webhooks
    12. GET List webhooks
    13. POST Subscribe to events
    14. DELETE Unsubscribe
    15. POST Fire a test delivery
  10. Webhooks
    1. Webhooks: events
    2. Webhooks: verify the signature
    3. Webhooks: retry policy
  11. Versioning
  12. Bug bounty
  13. Support
  14. llms.md

Production API contract #

Nova v1 is intentionally small. Keep integrations centered on these rules:

Minimal breakage: v1 keeps the current direct response shape. We add fields, headers, and endpoints without changing existing names or meanings.

Get API access #

You need three things:

  1. A Nova account. Create one if you don’t have it yet.
  2. 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).
  3. 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:

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...
One header only. Nova does not accept 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.

Recommended: keep all key usage on your backend. Use a thin proxy in your own infrastructure if you need to expose anything to a browser.

Rate limits #

Two budgets, both per API key:

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.

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

GET /api/v1/health Public

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

{ "ok": true, "service": "nova-public-api", "server_time": "ISO8601" }

Check the API is up

cURL
cURL
curl https://novadao.app/api/v1/health
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/health');
const data = await res.json();
console.log(data);
Python
Python
import httpx

res = httpx.get('https://novadao.app/api/v1/health')
print(res.json())

Returns

JSON
{
  "ok": true,
  "service": "nova-public-api",
  "server_time": "2026-04-25T12:34:56.000Z"
}
GET /api/v1/ping Bearer auth

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 header · string

    Bearer <your-api-key>

Response shape

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

Verify your key

cURL
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/ping
Node.js
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
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

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 Bearer auth Idempotency-Key required

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 header · string

    Bearer <your-api-key>

  • Idempotency-Key required header · string

    A unique value you generate per request (1..255 ASCII chars). `X-Idempotency-Key` is also accepted.

  • amount_in_cents required body · integer

    Charge amount in cents (BRL). Minimum 100 (R$1.00).

  • depix_address required body · string

    Confidential Liquid Network address (`lq1...`) that will receive the DePix once the charge settles.

Response shape

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

Create a R$50 charge

cURL
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
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
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

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 Bearer auth

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 path · string

    The charge id returned from POST /pix/charges.

Response shape

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

Check whether a charge was paid

cURL
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/pix/charges/ch_a8b3
Node.js
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
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

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"
}
GET /api/v1/webhooks Bearer auth

List webhooks.

Returns every webhook subscriber on your account.

Response shape

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

List configured webhooks

cURL
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/webhooks
Node.js
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
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

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 Bearer auth Idempotency-Key supported

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 body · string (URL)

    HTTPS endpoint that will receive POST deliveries.

  • events required body · string[]

    One or more from: pix.charge.created, pix.charge.received, pix.charge.paid, pix.charge.expired, pix.charge.failed, payment_link.paid.

Response shape

{ "id": number, "url": "...", "events": string[], "status": "active", "signing_secret": "<base64url, shown once>", "created_at": "ISO8601" }

Subscribe to paid charges

cURL
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
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
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

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 Bearer auth

Unsubscribe.

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

Parameters

  • id required path · integer

    Webhook id.

Response shape

{ "id": number, "deleted": true }

Remove a webhook

cURL
cURL
curl -X DELETE https://novadao.app/api/v1/webhooks/7 \
  -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
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
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

JSON
{
  "id": 7,
  "deleted": true
}
POST /api/v1/webhooks/:id/test Bearer auth

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 path · integer

    Webhook id.

Response shape

{ "delivery_id": "whd_...", "queued_at": "ISO8601" }

Send a signed test event

cURL
cURL
curl -X POST https://novadao.app/api/v1/webhooks/7/test \
  -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
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
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

JSON
{
  "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.

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
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
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
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);
}
Use the raw body. If your framework parses JSON before you can hash it, the signature won’t match — Express, Fastify, and Bun all expose the raw bytes via a setting or a middleware. Most failures we see in support are this.

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.

1m 5m 30m 2h 6h 24h

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

Rules

Out of scope

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_id from 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.