Developers
Getting started with the AntNexo API
Ten minutes from a key to your first webhook.
1. Make a key
In AntNexo, open Settings → Developers (owners and admins) and create a key. A read key only reads; a read and write key also adds posts, votes and comments for your customers. The key is shown once: keep it on your server, never in a browser or an app.
2. Your first request
Send the key as a bearer token. GET /api/v1/ext/me tells you which workspace it speaks for and its rate limit.
curl https://app.antnexo.com/api/v1/ext/me \
-H "Authorization: Bearer $ANTNEXO_KEY"const res = await fetch('https://app.antnexo.com/api/v1/ext/me', {
headers: { Authorization: `Bearer ${process.env.ANTNEXO_KEY}` },
});
console.log(await res.json()); // { workspace, key, rate_limit }import os, requests
res = requests.get("https://app.antnexo.com/api/v1/ext/me",
headers={"Authorization": f"Bearer {os.environ['ANTNEXO_KEY']}"})
print(res.json()) # {"workspace": ..., "key": ..., "rate_limit": ...}3. Lists and pages
Lists answer { items, next_cursor }. Pass next_cursor back as cursor for the next page, with the same filters; it is null on the last page. limit is 1 to 100, 25 by default.
curl "https://app.antnexo.com/api/v1/ext/posts?status=planned&limit=100" \
-H "Authorization: Bearer $ANTNEXO_KEY"
# then add &cursor=<next_cursor> until next_cursor is nullasync function* allPosts(query = 'status=planned') {
let cursor = null;
do {
const url = `https://app.antnexo.com/api/v1/ext/posts?${query}&limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`;
const page = await (await fetch(url, { headers: { Authorization: `Bearer ${process.env.ANTNEXO_KEY}` } })).json();
yield* page.items;
cursor = page.next_cursor;
} while (cursor);
}def all_posts(session, params):
params = {**params, "limit": 100}
while True:
page = session.get("https://app.antnexo.com/api/v1/ext/posts", params=params).json()
yield from page["items"]
if not page["next_cursor"]:
return
params["cursor"] = page["next_cursor"]4. Writing safely
Creates take an id you make (a UUID v7): sending the same create again returns the same row (200 instead of 201), so a retry after a lost answer never makes a duplicate. People are named by email: the customer is found or made, and their post or vote is theirs.
curl https://app.antnexo.com/api/v1/ext/posts \
-H "Authorization: Bearer $ANTNEXO_WRITE_KEY" -H "Content-Type: application/json" \
-d '{"id": "<a new UUID v7>", "board_id": "<board id>",
"title": "Export to CSV", "author": {"email": "[email protected]", "name": "Jane"}}'import { v7 as uuidv7 } from 'uuid';
const res = await fetch('https://app.antnexo.com/api/v1/ext/posts', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ANTNEXO_WRITE_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ id: uuidv7(), board_id, title: 'Export to CSV', author: { email: '[email protected]' } }),
}); // 201 new, 200 a retry of the same idimport uuid # Python 3.14+: uuid.uuid7(); before, the uuid6 package
res = session.post("https://app.antnexo.com/api/v1/ext/posts", json={
"id": str(uuid.uuid7()), "board_id": board_id, "title": "Export to CSV",
"author": {"email": "[email protected]"},
}) # 201 new, 200 a retry of the same id5. Rate limits
Your workspace's keys share its plan's requests per minute. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds); past the limit you get 429 rate_limited with Retry-After.
6. Webhooks
Add an endpoint in Settings → Developers and pick events: post.created, post.status_changed, post.merged, comment.created, changelog.published. AntNexo POSTs { id, type, created_at, workspace, data } with the resources as the API shows them, and tries again for about 13 hours when your endpoint fails. The same event may arrive twice: keep its id and skip repeats.
Checking the signature
Each delivery carries AntNexo-Signature: t=<unix time>,v1=<hex HMAC-SHA256 of "t.body" with your endpoint's secret>. Check it on the raw body, before parsing, and refuse old timestamps.
// Node 20+, Deno, Bun or any runtime with the global Web Crypto. rawBody is the request body as received.
async function verifyAntNexoSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(nowSeconds - t) > 300) return false; // older than 5 minutes: a replay
const enc = new TextEncoder();
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const mac = await crypto.subtle.sign('HMAC', key, enc.encode(t + '.' + rawBody));
const expected = Array.from(new Uint8Array(mac), (b) => b.toString(16).padStart(2, '0')).join('');
const got = parts.v1 ?? '';
if (got.length !== expected.length) return false;
let diff = 0; // compare every character, so timing says nothing about where they differ
for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ got.charCodeAt(i);
return diff === 0;
}import hashlib, hmac, time
def verify_antnexo_signature(secret: str, header: str, raw_body: bytes, now: "float | None" = None) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t = int(parts.get("t", "0"))
except ValueError:
return False # a broken header is a refusal, not an error
if not t or abs((now or time.time()) - t) > 300: # older than 5 minutes: a replay
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Versions
This is v1. It only grows: new endpoints, fields and values may appear, so ignore what you don't know. A change that would break your code would be a v2, next to v1.