Token gates API
Public check endpoint + authenticated CRUD. CRUD requires
IdentityOrgPermission.TokenGatesEdit / .TokenGatesView. The
/check endpoint is public — no auth required.
For the conceptual model — when to use gates, multi-wallet policies, caching — read Token gates.
Create a gate
POST /identity/organizations/:orgSlug/token-gates
Requires org.tokenGates.edit.
Request
{
name: string // 1–80 chars
description?: string // up to 500 chars
mintAddress: string // base58 Solana SPL mint
minHoldings: string // raw token units, decimal string
enabled?: boolean // default: true
}Response (201)
interface TokenGate {
id: string // 'gate_…'
org: string
name: string
description?: string
mintAddress: string
minHoldings: string
enabled: boolean
createdAt: string
updatedAt: string
}List gates
GET /identity/organizations/:orgSlug/token-gates
Requires org.tokenGates.view.
Returns { items: TokenGate[], total: number }.
Get one
GET /identity/organizations/:orgSlug/token-gates/:gateId
Same shape as a list item.
Update
PATCH /identity/organizations/:orgSlug/token-gates/:gateId
{
name?: string
description?: string | null
minHoldings?: string // raw token units
enabled?: boolean
}mintAddress is immutable — create a new gate if the token
changes.
Delete
DELETE /identity/organizations/:orgSlug/token-gates/:gateId
Returns 204.
Check access (public)
GET /identity/organizations/:orgSlug/token-gates/:gateId/check
No authentication. Public endpoint.
Query params
| Param | Type | Required | Notes |
|---|---|---|---|
walletAddress | string | yes | base58 Solana address |
refresh | true | no | Force fresh on-chain lookup, bypass cache. Rate-limited per IP. |
Response (200)
{
hasAccess: boolean
balance: string | null // raw units; null if wallet not found
reason: null | 'insufficientBalance' | 'gateDisabled' | 'walletNotFound'
}Behavior
- Balances are cached for 5 minutes by default. Refresh via
?refresh=true. - Cached responses include the cache age via
AgeHTTP header. - The endpoint never reveals
mintAddressorminHoldingsin the response.
Errors
| HTTP | code | When |
|---|---|---|
| 404 | not_found | Gate id does not exist or org slug is wrong. |
| 422 | validation_failed | walletAddress is not a valid base58 address. |
| 429 | rate_limited | Too many refresh=true calls from your IP. |
Bulk check (planned)
A bulk endpoint for checking N wallets against M gates in one round
trip is planned for the next release. Current workaround: call
/check per wallet × gate.
Privacy and abuse
- Gate IDs are part of the URL; they are not secret.
- Anyone with a gate id + a wallet address can run the check.
- The mint address and minimum holdings are not in the check response, only the boolean and balance.
- If your gate criteria are sensitive, don’t share the gate id with untrusted parties; treat it like a moderately secret config value.
Rate limits
| Endpoint | Limit |
|---|---|
POST/PATCH/DELETE /token-gates | 30/min/org |
GET /check (no refresh) | 600/min/IP |
GET /check?refresh=true | 60/min/IP |
Example
async function userHasAccess(userId: string, gateId: string) {
const wallets = await synq.users.publicKeys.list(userId)
// Any-wallet policy
for (const w of wallets) {
const r = await fetch(
`https://synq.id/identity/organizations/${ORG_SLUG}/token-gates/${gateId}/check?walletAddress=${w.address}`,
)
const { hasAccess } = await r.json()
if (hasAccess) return true
}
return false
}