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