The Gurb API accepts two credentials:
- a JWT bearer token, obtained by logging in — a person using the app;
- a community API key, sent as
X-Api-Key— an integration acting for one community (see API keys).
POST /api/auth/login{
"email": "admin@example.com",
"password": "your-password"
}The response contains a signed JWT. Send it on every subsequent request:
Authorization: Bearer <token>
The token payload carries userId, email, role, tenantId, and tenantSchema —
so the token itself identifies which tenant you are acting in.
Lifetime: 7 days by default (JWT_EXPIRES_IN).
Two endpoints exist:
| Endpoint | Auth required | Use |
|---|---|---|
POST /api/auth/refresh | Bearer token | Rotate while still valid |
POST /api/auth/refresh-token | none — send the refresh token in the body | Rotate after the access token expired |
Most endpoints are tenant-scoped. The API resolves which community you mean in this priority order:
- Custom domain — the request's hostname (highest priority)
communityIdin the URL — path, query, body, or theX-Community-IdheaderX-Tenant-Idheader- The JWT's tenant claim (lowest priority)
In practice, call tenant-scoped endpoints with the community in the path and send the tenant header as well:
Authorization: Bearer <token>
X-Tenant-Id: <tenantId>
Debugging tip. A
400 Tenant context requiredor401 No authentication token provideddoes not reliably mean your credentials are wrong. Six routers are mounted at the API root, so a URL that matches no route falls into their middleware before Express can conclude "no such route" — a typo'd path answers 400/401 instead of 404. Confirm the route exists before you debug your token.
A token is not membership. Reads that work without a token (optionalAuth: tweets,
events, blogs, albums, projects, projects 2, consultants and their lists) answer
according to what the caller IS in that community:
| Caller | Sees |
|---|---|
| APPROVED member | the community's content; other members' drafts and hidden items stay hidden unless the member moderates that section |
| guest, or a signed-in non-member | only items the community opted into guest preview, and only in a PUBLIC community whose plan includes the section |
| anyone else | an empty list, or 404 |
Something you may not see answers 404, never 403, so a response never
confirms that a private item exists. The same applies to what hangs off an item: an
event's or album's comments, likes and attendees are readable exactly when the item is.
| Status | Code | Meaning |
|---|---|---|
401 | UNAUTHORIZED | Missing, malformed, or expired token |
403 | NOT_A_COMMUNITY_MEMBER / membership message | Not an APPROVED member of this community (left, banned, pending) — every write requires membership |
403 | Forbidden + Missing permission: … | A member, but lacking the permission. Editing or deleting someone else's content needs the section's manage or moderate permission; *:create only ever covers your own. Groups have no owner: editing any group needs groups:manage or groups:moderate |
403 | RESTRICTION_BLOCKED | The member is suspended or blocked from this action (posting, commenting, media, likes, RSVPs, events). Applies to API keys through their owner too |
403 | FEATURE_NOT_AVAILABLE | The community's plan does not include this feature |
403 | FEATURE_DISABLED | The plan may include it, but the community's admin switched the section off (projects 2) |
400 | TENANT_CONTEXT_REQUIRED | No community could be resolved from the request |
400 | COMMUNITY_MISMATCH | A communityId in the body disagrees with the community the request was authorized for |
404 | …_NOT_FOUND | Missing, or not visible to you (see above) |
409 | PROJECT_REQUEST_NOT_PENDING / JOIN_REQUEST_NOT_PENDING / INVITATION_NOT_PENDING | Already decided — including by someone acting at the same moment. Exactly one approve/reject wins |
Always read the response body: the 403s come from different checks and are
fixed by different people (an admin, a moderator, the community admin's settings, or the
billing owner).
FEATURE_NOT_AVAILABLE includes a featureKey field naming the feature that is off.
A community admin with api_keys:manage issues keys for integrations:
POST /api/communities/{communityId}/api-keys # create — the key is shown ONCE
GET /api/communities/{communityId}/api-keys # list (prefix only, never the key)
DELETE /api/communities/{communityId}/api-keys/{id}- Format
gurb_<12 hex>_<secret>. Only a hash is stored; the full key is returned once, at creation. Thegurb_<12 hex>prefix identifies it afterwards. - Scopes:
read,write(default both). OptionalexpiresAt, up to 10 years. - Send it as
X-Api-Key: <key>to the SDK surface,/api/sdk/…. The key is the community: it was pinned at creation, and nothing in the request can point it elsewhere. - A key acts as the admin who created it, and is checked the way that person would be, on every request: they must still be an APPROVED member, hold the permission (or own the content), and not be restricted. Demote or remove the owner and the key loses that reach with them.
- Keys cannot manage keys: the key-management routes refuse API-key authentication.