Guidely API reference
All endpoints are served by apps/web under /api/v1. Responses are JSON. Errors use a consistent envelope:
{ "error": { "code": "validation_error", "message": "…", "details": { } } }
Error codes: unauthorized (401), forbidden (403), not_found (404), validation_error (400), conflict (409), rate_limited (429), internal (500).
List endpoints accept ?page=1&pageSize=25 (max 100) and return { items, page, pageSize, total }.
Public SDK API (/api/v1/sdk/*)
Authentication: the environment public key, passed as ?key=pk_… or header X-Guidely-Key. Origin is checked against the environment's allowed origins (when configured); native and React Native apps send no Origin and are not restricted by it. CORS headers are set for the allowed origin; preflight OPTIONS is supported. Rate limited per key and per IP. Public keys only unlock published data for that environment; they can never read drafts, analytics, or other environments.
| Method | Path | Body | Response | ||
|---|---|---|---|---|---|
| GET | /sdk/config?platform= | – | SdkConfigResponse (ETag = W/"<projectId>:<env>:<configVersion>[:<platform>]", honours If-None-Match → 304). platform = web \ | ios \ | android returns only guides enabled for that platform; omitted, all guides are returned (older web SDKs). |
| POST | /sdk/identify | IdentifyRequest | IdentifyResponse | ||
| POST | /sdk/events | EventBatch | { accepted: number, duplicates: number }; 503 when an exception's source map exists but can't be fetched right now: the rest of the batch is stored and a retry adds the exception (stored events are skipped as duplicates) | ||
| POST | /sdk/replay[?enc=gzip] | ReplayChunk as JSON, or gzip-compressed JSON with enc=gzip (text/plain; ≤2 MB compressed, ≤16 MB raw) | 202 { accepted }, { accepted: 0, duplicate: true }, or { accepted: 0, disabled: true } when replay is off for the project; 409 when the session id belongs to another visitor; 413 over the size limits (also enforced on bodies without Content-Length); 503 when recording storage is unavailable (retry) | ||
| POST | /sdk/guide-state | GuideStateUpdate | { ok: true } | ||
| POST | /sdk/checklists/:guideId/progress | ChecklistProgressRequest | { completedTaskIds, completedAt, dismissedAt } | ||
| POST | /sdk/surveys/:guideId/responses | SurveyResponseRequest | { ok: true, duplicate?: true } | ||
| GET | /sdk/resource-center/search?q= | – | { articles: ResourceCenterArticle[] } (full-text, published articles; last word matches as a prefix) | ||
| GET | /sdk/articles | – | SdkArticlesResponse (published articles without bodies, and collections; parentId / audienceIds only when set) | ||
| GET | /sdk/articles/:id | – | { article: ResourceCenterArticle } | ||
| POST | /sdk/conversations | StartConversationRequest | StartConversationResponse (201 JSON), or a text/event-stream of ScoutStreamEvents when Scout replies (first event conversation carries the token) | ||
| POST | /sdk/conversations/lookup | { tokens: string[] } | ConversationLookupResponse | ||
| GET | /sdk/conversations/:id/messages?since= | – | ConversationMessagesResponse | ||
| POST | /sdk/conversations/:id/messages | { message } | JSON snapshot, or an event stream when Scout replies | ||
| POST | /sdk/conversations/:id/handoff | – | ConversationMessagesResponse | ||
| POST | /sdk/conversations/:id/rating | { rating: 1–5, comment? } | { conversation } | ||
| GET | /sdk/authoring/session?token= | – | AuthoringSessionInfo | ||
| POST | /sdk/authoring/session/picks?token= | AuthoringPick | { ok: true } | ||
| POST | /sdk/authoring/session/heartbeat?token= | – | { ok: true } | ||
| GET | /sdk/capture/session?token= | – | CaptureSessionInfo (how-to recording; 401 once it ended or expired) | ||
| POST | /sdk/capture/steps?token= | multipart: step (CaptureStep JSON), optional screenshot (PNG/JPEG/WebP, ≤8 MB; Content-Length required) | 201 { id, position, stepCount }; a retry with the same clientId returns the stored step | ||
| POST | /sdk/capture/session/heartbeat?token= | – | { ok: true } | ||
| POST | /sdk/capture/session/finish?token= | – | { ok: true, howToId } | ||
| GET | /sdk/health | – | { ok: true, environment, project } |
Types live in packages/shared/src/sdk-api.ts, capture.ts, howto.ts and messenger.ts. Authoring and capture routes authenticate with their session token instead of the public key. :guideId accepts the guide id or key.
Conversation routes additionally require the per-conversation token returned when the conversation was created, in header X-Guidely-Conversation; a wrong or missing token answers 404. Request bodies are limited to 32 KB. Event streams send message, delta ({ messageId, text }), done ({ message, conversation }) and error events, plus : keepalive comments every 15 s.
Guide display never depends on event ingestion succeeding; events are queued and retried by the SDK.
Management API (/api/v1/*)
Authentication: guidely_session httpOnly cookie. Mutating requests must come from the dashboard origin (Origin/Sec-Fetch-Site check) for CSRF protection. Every route resolves the workspace from the addressed resource and checks the caller's membership role, or one of the member's permissions (role defaults plus per-member grants and revokes, see apps/web/src/lib/permissions.ts):
| Role | Capabilities |
|---|---|
| OWNER | everything incl. deleting workspace, billing, transferring ownership |
| ADMIN | manage members/projects/environments/integrations, publish anywhere |
| EDITOR | create/edit guides, audiences, content; publish to development & staging; publish to production when project setting editorsCanPublishProduction is true |
| VIEWER | read-only, including analytics and session replays |
Permissions an admin can grant or revoke per member (default role in brackets): guides.edit, guides.publish, audiences.edit, articles.edit, articles.publish (an article's published flag and publishing the resource center), inbox.reply, insights.manage (editor); guides.publishProduction, guides.delete, scout.manage, userData.delete, project.settings, developer.manage (admin); replays.view (viewer). Owners always hold all of them. Managing members, sign-in, the audit log and the workspace stay tied to roles. Routes marked "(editor)" or "(admin)" below check the matching permission. GET /projects/:id returns the caller's permissions.
Auth & account
POST /auth/signup { email, name, password, workspaceName?, inviteToken? } · POST /auth/login { login | email (email or username), password, inviteToken? } → { user, workspaceId, inviteError } · POST /auth/logout · GET /auth/me · PATCH /auth/me { name?, username?, currentPassword?, newPassword? } · GET /auth/sessions · DELETE /auth/sessions/:id · GET /auth/providers?workspace=<slug> (public; enabled methods, narrowed to the workspace's) · GET /auth/github and GET /auth/google ?next=&inviteToken=&workspace= (browser redirects; callbacks at /auth/<provider>/callback)
A member whose session signed in with a method their workspace doesn't allow gets 403 with error.details = { reason: "auth_method", workspaceSlug, allowedAuthMethods }. GET /auth/me lists such workspaces with locked: true and no projects.
Workspaces
GET /workspaces · POST /workspaces · GET /workspaces/:id · PATCH /workspaces/:id · DELETE /workspaces/:id (OWNER) · POST /workspaces/:id/leave (not the last owner) · GET /workspaces/:id/members · PATCH /workspaces/:id/members/:userId { role } · PATCH /workspaces/:id/members/:userId/permissions { grants, revokes } (ADMIN; not your own; only owners for admins; owners can't be restricted) · DELETE /workspaces/:id/members/:userId · POST /workspaces/:id/invitations { email, role, grants?, revokes? } (the access is applied when the invitation is accepted) · GET /workspaces/:id/invitations · DELETE /workspaces/:id/invitations/:invId · POST /invitations/accept { token } · GET /workspaces/:id/audit-log · GET /workspaces/:id/sign-in · PATCH /workspaces/:id/sign-in { allowedAuthMethods?, githubOrgs?, googleDomains?, autoJoinRole? } (ADMIN) · POST /workspaces/:id/logins { name, email, username?, password, role, grants?, revokes? } (ADMIN; workspace-managed login) · POST /workspaces/:id/logins/:userId/password { password } (ADMIN; managed logins only)
Platform admin
Only for platform admins (see security); everyone else gets 404. Workspace changes are written to that workspace's audit log as admin.* actions; deletes and admin grants are logged centrally.
GET /admin/workspaces?q=&page=&pageSize= (search by name, slug or member email) · POST /admin/workspaces { name, ownerEmail } · GET /admin/workspaces/:id (members, projects) · PATCH /admin/workspaces/:id { name } · DELETE /admin/workspaces/:id · POST /admin/workspaces/:id/members { email, role } · PATCH /admin/workspaces/:id/members/:userId { role } · DELETE /admin/workspaces/:id/members/:userId · GET /admin/admins · POST /admin/admins { email } · DELETE /admin/admins/:userId
Projects & environments
GET /workspaces/:id/projects · POST /workspaces/:id/projects · GET /projects/:id · PATCH /projects/:id (name, description, theme, settings) · POST /projects/:id/theme/import ({ url } → { url, theme, palette }: branding read from a public website, not saved; admins, 20 per 10 minutes) · DELETE /projects/:id · GET /projects/:id/environments · PATCH /projects/:id/environments/:envId (allowedOrigins, settings) · POST /projects/:id/environments/:envId/rotate-key · GET /projects/:id/status (SDK connectivity, last events) · GET /projects/:id/setup (installation snippets)
Guides
GET /projects/:id/guides?kind=&status=&q=&sort=&page= · POST /projects/:id/guides { name, kind, key?, templateId?, description? } · GET /guides/:id · PATCH /guides/:id (name, description, key, tags, draftConfig) · DELETE /guides/:id · POST /guides/:id/duplicate · POST /guides/:id/archive · POST /guides/:id/unarchive · POST /projects/:id/guides/bulk { ids, action } POST /guides/:id/validate → { issues } · POST /guides/:id/publish { environment, notes? } → version · POST /guides/:id/rollback { environment, version } · POST /guides/:id/pause { environment } · POST /guides/:id/resume · POST /guides/:id/unpublish · GET /guides/:id/versions · GET /guides/:id/versions/:version · GET /guides/:id/history (publish events) GET /templates
Audiences
GET /projects/:id/audiences · POST · GET /audiences/:id · PATCH · DELETE · GET /audiences/:id/members · POST /audiences/:id/members { externalIds } · DELETE /audiences/:id/members/:endUserId · POST /projects/:id/audiences/preview { rules } → estimated size
End users & events
GET /projects/:id/users?q= · GET /projects/:id/users/:id · DELETE /projects/:id/users/:id (GDPR deletion) · GET /projects/:id/accounts · GET /projects/:id/events?env=&name=&from=&to=&userId=&guideId=&page= · GET /projects/:id/events/export.csv
Analytics
GET /projects/:id/analytics/overview?env=&from=&to=&compare= · GET /projects/:id/analytics/guides?env=&from=&to= · GET /guides/:id/analytics?env=&from=&to= (funnel, step abandonment, timeseries, audience comparison) · GET /guides/:id/analytics/export.csv · GET /guides/:id/responses (surveys) · GET /guides/:id/responses/export.csv · GET /guides/:id/checklist-progress
Product analytics
Response types: apps/web/src/lib/product-analytics-types.ts. Details in product-analytics.md. GET /projects/:id/replays?env=&from=&to=&userId=&sessionId=&minDurationMs=&hasErrors=1&hasRageClicks=1&url=&guideId=&issueId=&hasNetworkErrors=1&console=&consoleLevel=error,warn&page= · GET /replays/:id (metadata + session event timeline) · GET /replays/:id/recording (rrweb events) · DELETE /replays/:id (admin) GET /projects/:id/pages?env=&from=&to=&groupIds=1&q=&compare= · GET /projects/:id/pages/detail?path=&env=&from=&to=&groupIds=1 · GET /projects/:id/heatmaps?path=&viewport=desktop|tablet|mobile&env=&from=&to=&groupIds=1 GET /projects/:id/errors?env=&from=&to=&status=open|resolved|ignored|all&q=&userId=&sort=lastSeen|firstSeen|occurrences|users&page= · GET /errors/:id?env=&from=&to= · PATCH /errors/:id { status } (editor) GET /projects/:id/replay-alert-rules · POST /projects/:id/replay-alert-rules (editor) · PATCH /replay-alert-rules/:id · DELETE /replay-alert-rules/:id · POST /replay-alert-rules/:id/test (editor) · GET /projects/:id/replay-alert-rules/deliveries?ruleId=&page= GET /projects/:id/alert-rules · POST /projects/:id/alert-rules (editor) · PATCH /alert-rules/:id · DELETE /alert-rules/:id · POST /alert-rules/:id/test (editor) · GET /projects/:id/alert-rules/deliveries?ruleId=&page= POST /projects/:id/insights/funnel { env?, from?, to?, definition: { steps, windowHours } } · POST /projects/:id/insights/retention { env?, from?, to?, definition: { startEvent, returnEvent, period, periods } } · GET /projects/:id/insights · POST /projects/:id/insights { kind, name, definition } (editor) · PATCH /insights/:id · DELETE /insights/:id (editor)
Source maps and upload keys
GET /projects/:id/api-keys · POST /projects/:id/api-keys { name, scopes: ["sourcemaps:write"] } → { …, token } (shown once; admin) · DELETE /api-keys/:id (revoke; admin) · GET /projects/:id/sourcemaps?release=&q=&page= · DELETE /sourcemaps/:id · DELETE /projects/:id/sourcemaps?release= (admin)
Build-time API, authenticated with Authorization: Bearer gly_sk_… (scope sourcemaps:write): POST /sourcemaps/upload (multipart: release?, manifest = [{ field, path }], one file field per map; ≤25 MB per map, ≤200 maps) → { release, uploaded: [{ path, sizeBytes, sources, hasContent, replaced }], errors: [{ path, message }] } · GET /sourcemaps/whoami. Use the @guidelytech/sourcemaps CLI rather than calling these directly.
Resource center & integrations
GET /projects/:id/resource-center · PATCH · POST /projects/:id/resource-center/publish · …/collections CRUD (parentId nests a collection, at most 3 levels; deleting one moves its articles and sub-collections to its parent) · …/articles CRUD (collections and articles take audienceIds: empty = everyone) · POST /projects/:id/media (editor; the raw file is the body with an exact Content-Length, optional X-File-Name; PNG/JPEG/GIF/WebP ≤10 MB, MP4/WebM ≤200 MB, PDF ≤50 MB, detected from the bytes) → 201 { id, url, contentType, size, kind: "image"|"video"|"file", fileName } · GET /media/:projectId/:file (public, supports Range) · GET /projects/:id/integrations · POST · PATCH /integrations/:id · DELETE (secret fields such as webhook URLs are returned masked; sending the masked value back keeps the stored secret)
Issue trackers & tickets
GET /workspaces/:id/ticket-integrations (viewer; never returns the key/token, only config.secretHint) · POST /workspaces/:id/ticket-integrations/verify (admin; { provider: "linear", apiKey? } → teams, or { provider: "jira", siteUrl, email, apiToken?, projectKey? } → projects and that project's issue types; a missing secret uses the saved one, for Jira only while site and email are unchanged) · POST /workspaces/:id/ticket-integrations (admin; connect or replace: Linear { teamId }, Jira { projectKey, issueType }; only https://<site>.atlassian.net hosts) · PATCH /ticket-integrations/:id (name, enabled) · DELETE · GET /projects/:id/tickets?sourceType=error|replay|conversation&sourceId= · GET /projects/:id/tickets/draft?sourceType=&sourceId= (editor; { title, description } in Markdown) · POST /projects/:id/tickets (editor; { integrationId, sourceType, sourceId, title, description } → 201 with key and url). Error and replay alert rules take ticketIntegrationId (one ticket per issue / recording; test alerts never create one).
Scout, inbox & knowledge imports
GET /projects/:id/scout · PUT (admin; anthropicApiKey sets the project's write-only Anthropic key, null removes it; responses carry aiKeySource and anthropicKeyHint, never the key) · GET /projects/:id/scout/stats · POST /projects/:id/scout/playground { messages, environment?, instructions?, productName?, productContext? } → event stream (delta, done, error; editor) · GET /projects/:id/conversations?filter=needs_reply|scout|closed|mine|all&page= · GET /projects/:id/conversations/unread · GET /conversations/:id · PATCH /conversations/:id { status?, assigneeId? } (editor) · POST /conversations/:id/messages { message } (editor) · GET /projects/:id/knowledge/imports · POST (multipart file, collectionId?; ≤10 MB; editor) · POST /projects/:id/knowledge/imports/intercom { accessToken, region?: "us"|"eu"|"au", collectionId?, publish? } (editor; token checked against Intercom, kept encrypted until the import finishes, never returned; without collectionId Intercom's collections are recreated; articles already imported are skipped) · GET /knowledge/imports/:id · DELETE (editor)
Authoring
POST /guides/:id/authoring-sessions { environment, targetUrl } → { id, token, url } · GET /authoring-sessions/:id (picks, status) · DELETE /authoring-sessions/:id
How-tos
Reading needs viewer; everything else the howtos.edit permission (editors by default). See how-tos.md. GET /projects/:id/how-tos?q=&page= · POST /projects/:id/how-tos { title, description? } · POST /projects/:id/how-tos/capture { environment, targetUrl, howToId?, title? } → { howToId, sessionId, url } (open url: it carries ?guidely_capture=<token>) · DELETE /capture-sessions/:id (stop recording) · GET /how-tos/:id (steps, sharing, capture while a recording is open) · PATCH { title?, description? } · DELETE (screenshots are deleted with it) · POST /how-tos/:id/duplicate · POST /how-tos/:id/steps { kind: text|heading|tip|alert, title?, body?, afterId?, atStart? } · PUT /how-tos/:id/steps/order { order: stepId[] } · PATCH /how-tos/:id/steps/:stepId { title?, body?, showHighlight?, zoom?, highlight? } · DELETE · PUT /how-tos/:id/steps/:stepId/screenshot { mediaId | null } (an image uploaded with POST /projects/:id/media) · POST /how-tos/:id/steps/:stepId/redact { regions: NormalizedRect[] } (pixelated on the server; the original is deleted) · POST /how-tos/:id/share { enabled, rotate? } → shareUrl (/share/:token) and embedUrl (/embed/:token) · GET /how-tos/:id/export?format=md|html (download) · POST /how-tos/:id/ai (rewrite with the project's Anthropic key; 503 without one) · POST /how-tos/:id/tour (draft flow guide; needs guides.edit too) → { guideId } · POST /how-tos/:id/article { publish? } (create or update the linked help article; needs articles.edit, and articles.publish to publish) · public: POST /share/:token/view (counts a view, once per viewer IP every 30 minutes)