REST API
Read your server's gates, members, leaderboard, and audit log over a simple authenticated REST API.
HoneyGate exposes a read-only REST API so you can pull your server's data into your own apps, dashboards, or websites. It's on every plan, Free included. This is infrastructure, not an upsell.
Authentication
Create an API key in the admin panel under API Keys (/admin/public-api/). The key is shown once at creation, since we only store a hash, so copy it right then.
Send it as a Bearer token:
Authorization: Bearer $HONEY_GATE_API_KEY
Keys start with hg_live_ and run about 72 characters. Each one is scoped, so you pick what it can read when you create it:
| Scope | Grants |
|---|---|
gates.read | List gates and their conditions |
members.read | List members (with XP and join dates) |
points.read | Read points totals, the leaderboard, and rank |
points.write | Award guild currency exactly once |
points.adjust | Reverse a prior API award exactly once |
audit.read | Read audit-log entries |
holdings.read | Read a member's on-chain holdings (NFT counts and token balances) for your server's tracked assets |
snapshots.read | Read complete cached NFT holder snapshots, including wallet addresses, for collections you explicitly approve for the key |
There's also an events.read scope for the live Event Stream. That one isn't a REST endpoint on this page; see Outbound webhooks for push-style updates.
A key is tied to one server, and you can hold up to 10 active keys per server. Give a key an optional expiry if you want it to age out.
Base URL & format
Every endpoint lives under:
https://honeygate.app/api/v1/public-api/<endpoint>
Responses are JSON. Success looks like this:
{ "ok": true, "data": { ... }, "request_id": "..." }
Errors look like this:
{ "ok": false, "error": { "message": "...", "code": "..." }, "request_id": "..." }
Common status codes: 200 OK, 401 invalid or expired key, 403 missing scope, 404 unknown endpoint, 429 rate-limited.
Rate limit
120 requests per minute per key by default, and it's configurable per key. Go over and you'll get a 429. Wait, then retry.
Endpoints
GET /gates (scope: gates.read)
Lists the server's gates, each with its role, logic mode (AND/OR), and conditions.
Every condition includes both threshold fields:
min_amountis the raw value stored in the gate configuration, retained for compatibility.min_amount_enforcedis the value HoneyGate actually compares during evaluation. NFT and trait counts are rounded up to a whole item and never below 1; fungible-token amounts pass through.
Use min_amount_enforced when displaying or independently evaluating what the live gate requires.
GET /members (scope: members.read)
Lists members with discord_id, username, display name, joined_at, left_at, is_bot, and xp. Supports ?limit= (default 100, max 1000) and ?offset= for paging.
GET /member?discord_id=… (scope: members.read)
Looks up one member instead of paging through everyone. Returns their username, display name, avatar, joined_at, xp, and:
in_server—trueorfalse. A member's record is kept after they leave so older history still makes sense, so "we found a record" is not the same as "they are still here." This field says which.staff— their standing in your server:levelisowner,admin,stafformember, alongsideis_owner/is_admin/is_staffbooleans. An owner counts as an admin, and an admin counts as staff.
Use staff to decide whether to let somebody into your own admin screens.
It answers about your server only. A HoneyGate platform administrator is not reported as staff of your server, and neither is anyone holding an older platform-wide grant. The question this answers is "does this person run things here", which is the one worth acting on.
GET /leaderboard (scope: points.read)
Top members by XP. Supports ?limit= (default 25, max 500).
GET /audit (scope: audit.read)
Audit-log entries: role grants and revokes, gate passes, config changes. Supports ?limit= (default 100, max 500) and ?since=<ISO datetime> (defaults to the last 7 days).
GET /holdings?discord_id=… (scope: holdings.read)
How much of your server's tracked assets a member holds on-chain. It reads the same warm balance/NFT cache HoneyGate uses for its own gate checks, aggregated across every wallet that member has verified.
It answers only about the assets your server tracks (the ones you add under web3 asset tracking). A wallet holding some other token you don't track is never reported.
Numbers are exact and integer-based, on purpose:
- Tokens return
amount_raw— the balance in the token's smallest unit, as a string — plusdecimals. Render it asamount_raw ÷ 10^decimals. We never send a floating-point token amount, because float rounding loses precision on large balances. - NFT collections return a whole
count.
Each asset, and the response overall, carries freshness evidence:
checked_at(overall) — when this response was produced (UTC).data_as_of(overall) — the oldest underlying cache timestamp that fed it.checked_at(per asset) — when that asset's data was last read on-chain.complete(overall) —trueonly if every tracked asset could be read.
It fails closed, never to a false zero. If an asset's on-chain data can't be read right now (for example the server's Solana provider is briefly unreachable), that asset comes back "available": false with a reason and no amount — never 0. A 0 here always means "we checked and they hold none"; it never means "we couldn't find out." Treat complete: false as "ask again shortly," not as "they hold less."
It returns a wallet_count so you know how many verified wallets fed the answer. It does not return the wallet addresses themselves — a holdings total doesn't require handing out someone's wallets.
{
"ok": true,
"data": {
"guild_id": "123456789012345678",
"discord_id": "DISCORD_ID",
"wallet_count": 2,
"checked_at": "2026-08-13 12:00:00",
"data_as_of": "2026-08-13 11:42:00",
"complete": true,
"assets": [
{
"asset_id": "fook",
"label": "$FOOK",
"type": "token",
"mint": "Foo...mint",
"units": "$FOOK",
"available": true,
"amount_raw": "1500000000",
"decimals": 6,
"checked_at": "2026-08-13 11:42:00",
"from_cache": true
},
{
"asset_id": "zombabiez",
"label": "Animated ZomBabieZ",
"type": "nft_collection",
"mint": "Col...addr",
"available": true,
"count": 3,
"checked_at": "2026-08-13 11:50:00"
}
]
}
}
curl "https://honeygate.app/api/v1/public-api/holdings?discord_id=DISCORD_ID" \
-H "Authorization: Bearer $HONEY_GATE_API_KEY"
GET /holder-snapshot?asset_id=…&min_count=… (scope: snapshots.read)
Returns the wallet address and whole NFT count for every holder meeting min_count in one of your server's tracked NFT collections. asset_id is the tracked asset's short ID. min_count defaults to 1 and can be changed on every request, so an outside app can build different claim rounds without changing HoneyGate.
This endpoint exposes wallet addresses, so it has stricter access than /holdings: the API key must have snapshots.read, and you must explicitly save the collection as an approved asset on that key. Older keys with no asset restriction are refused.
The response includes snapshot_at, snapshot_age_seconds, total_holder_count, eligible_count, and complete: true. HoneyGate keeps collections approved for active snapshot keys warm in a bounded background job every 10 minutes. The public request itself reads the existing cache only; it does not make an on-chain request while answering. If the cache is stale, missing, malformed, or incomplete, the request fails with 503 instead of returning a partial allowlist.
{
"ok": true,
"data": {
"asset": { "asset_id": "pixel-zombabiez", "type": "nft_collection" },
"min_count": 41,
"snapshot_at": "2026-08-18 12:00:00",
"eligible_count": 27,
"complete": true,
"holders": [{ "wallet": "...", "count": 54 }]
}
}
POST /points-award (scope: points.write) — writes
Awards Coins to a member. This is the only endpoint that changes anything, so it has its own scope: a key that reads your leaderboard cannot also create currency. Grant points.write only to an app you trust.
Send JSON:
| Field | Required | Notes |
|---|---|---|
discord_id | yes | The member's Discord ID. They must be in your server. |
amount | yes | A whole number above zero, up to 1,000,000 per call. |
idempotency_key | yes | Any string unique to this award — your own play or transaction id works well. |
source | no | A short label that shows in the ledger, e.g. arcade_win. Defaults to external_api. |
note | no | A human-readable note, up to 500 characters. |
The idempotency_key is the important one. If a request times out you have no way of knowing whether it arrived, so you have to retry — and a retry must not pay someone twice. Send the same key again and nothing is credited the second time:
{ "ok": true, "data": { "applied": false, "balance": 1250, "amount": 250, ... } }
applied: false means "this one was already counted." That is a success, not an error — the player has their Coins. Retrying a timed-out request with the exact same key is always safe, and is what you should do.
GET /points-balance?discord_id=… (scope: points.read)
Returns one member's authoritative spendable balance for the API key's server, plus that server's configured currency name and abbreviation. The server is taken only from the API key; callers cannot select another server in the request.
GET /points-history?discord_id=… (scope: points.read)
Returns up to 100 of one member's newest ledger entries for the API key's server. Use limit and the optional before_id cursor to page backward. Entries contain the signed amount, source, note, idempotency evidence, and any reversal link. Another server's member history is never returned.
POST /points-spend (scope: points.spend) — writes
Deducts a positive whole-number amount from one member. Send discord_id, amount, a required idempotency_key, and optional source and note. HoneyGate locks the member's balance, refuses overdrafts, makes retries exactly-once, and rejects reuse of a key with different data. This only reduces the internal ledger; it does not sign or send an on-chain transaction.
POST /points-reverse (scope: points.adjust) — writes
Creates one exact compensating entry for a prior /points-award. Send the original award's idempotency_key, a new idempotency key for the reversal, and a required human reason. HoneyGate refuses unknown awards, awards belonging to another server, reused keys with different data, a second reversal of the same award, or a reversal that would make the member's balance negative. This is intentionally not a general debit endpoint.
Example
curl https://honeygate.app/api/v1/public-api/leaderboard?limit=10 \
-H "Authorization: Bearer $HONEY_GATE_API_KEY"
Awarding Coins:
curl -X POST https://honeygate.app/api/v1/public-api/points-award \
-H "Authorization: Bearer $HONEY_GATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"discord_id":"123456789012345678","amount":250,"idempotency_key":"play-abc123","source":"arcade_win"}'
Want live updates instead of polling?
For push-style integration, reach for Outbound webhooks. HoneyGate calls your URL when things happen, so you skip the polling entirely.