Download OpenAPI specification:
REST API for Granite clients, bots, and integrations.
Conventions (apply to every operation; see PLAN.md 3.2 and 3.6):
https://api.granite.md. All public operations live under /v1.
After Phase 6 ships, /v1 changes are additive only.application/problem+json, schema Problem).
The type URI is https://granite.md/problems/<slug>; code refines it for programmatic handling.
Every response carries X-Request-ID, also echoed as request_id in problems.cursor and limit and return
{ "items": [...], "next_cursor": "..." }. next_cursor is absent on the last page.
Cursors are opaque; clients must not parse or build them.POST operations accept an Idempotency-Key header
(1-255 printable ASCII). Repeating a request with the same key and body within 24 hours
returns the original response; the same key with a different body returns 409 with
code idempotency_key_reused.Authorization: Bearer <token> with a session access token,
a personal API token (gr_pat_...), or an OAuth access token. Scopes are listed per operation.429 with code token_rate_limited and a Retry-After header (seconds).Crash and error reports from the apps (PLAN.md P5-T01), written to the server log for the on-call person. No sign-in is needed (an app can fail before it). Reports must not contain note text, file names, or anything the person typed. Limited per address.
| kind required | string Enum: "crash" "error" "render" |
| message required | string <= 1000 characters |
| stack | string <= 8000 characters |
| screen | string <= 200 characters |
| platform required | string Enum: "web" "macos" "windows" "linux" "ios" "android" "cli" "other" |
| app_version required | string <= 50 characters |
| os_version | string <= 50 characters |
{- "kind": "crash",
- "message": "string",
- "stack": "string",
- "screen": "string",
- "platform": "web",
- "app_version": "string",
- "os_version": "string"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Creates an unverified account and emails a verification link. The response is the same whether or not the email is already registered (the existing owner is notified).
| email required | string <email> <= 254 characters |
| password required | string [ 10 .. 256 ] characters |
| name | string <= 200 characters |
{- "email": "user@example.com",
- "password": "stringstri",
- "name": "string"
}{- "status": "accepted"
}| token required | string <= 200 characters |
{- "token": "string"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Always answers 202, whether or not the email has an unverified account.
| email required | string <email> <= 254 characters |
{- "email": "user@example.com"
}{- "status": "accepted"
}Checks the password and starts a session for the device. After repeated failures the email is locked for a while (429), whether or not it has an account.
| email required | string <email> <= 254 characters |
| password required | string <= 256 characters |
required | object (DeviceInfo) |
{- "email": "user@example.com",
- "password": "string",
- "device": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "Fuad's MacBook",
- "platform": "web",
- "app_version": "string"
}
}{- "access_token": "string",
- "token_type": "Bearer",
- "expires_in": 0,
- "refresh_token": "string",
- "refresh_expires_in": 0,
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "user": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "user@example.com",
- "name": "string",
- "email_verified": true,
- "two_factor_enabled": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}Finds the workspace that verified the email's domain and set up SSO, and returns the
identity provider's sign-in URL. Open it in the browser; the provider returns to the
web app's /sso/callback, which finishes with POST /v1/auth/sso/finish.
| email required | string <= 320 characters |
{- "email": "string"
}{- "authorize_url": "string"
}Exchanges the code from the identity provider and starts a session. People new to the workspace join it with the connection's role. Two-step verification is left to the identity provider.
| state | string <= 200 characters |
| code | string <= 4000 characters |
| handoff | string <= 200 characters |
required | object (DeviceInfo) |
{- "state": "string",
- "code": "string",
- "handoff": "string",
- "device": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "Fuad's MacBook",
- "platform": "web",
- "app_version": "string"
}
}{- "access_token": "string",
- "token_type": "Bearer",
- "expires_in": 0,
- "refresh_token": "string",
- "refresh_expires_in": 0,
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "user": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "user@example.com",
- "name": "string",
- "email_verified": true,
- "two_factor_enabled": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}Exchanges a refresh token for a new access token and a new refresh token. Each refresh
token works once; presenting a used one ends every session of that sign-in
(code refresh_token_reused). Clients must not refresh concurrently.
| refresh_token required | string <= 200 characters |
{- "refresh_token": "string"
}{- "access_token": "string",
- "token_type": "Bearer",
- "expires_in": 0,
- "refresh_token": "string",
- "refresh_expires_in": 0,
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "user": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "user@example.com",
- "name": "string",
- "email_verified": true,
- "two_factor_enabled": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Always answers 202, whether or not the email has an account.
| email required | string <email> <= 254 characters |
{- "email": "user@example.com"
}{- "status": "accepted"
}Sets the password from a reset link and signs out every device.
| token required | string <= 200 characters |
| password required | string [ 10 .. 256 ] characters |
{- "token": "string",
- "password": "stringstri"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Send either code (from the authenticator app) or recovery_code. Each works once.
| mfa_token required | string <= 2000 characters |
| code | string <= 10 characters |
| recovery_code | string <= 40 characters |
{- "mfa_token": "string",
- "code": "string",
- "recovery_code": "string"
}{- "access_token": "string",
- "token_type": "Bearer",
- "expires_in": 0,
- "refresh_token": "string",
- "refresh_expires_in": 0,
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "user": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "user@example.com",
- "name": "string",
- "email_verified": true,
- "two_factor_enabled": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}Re-checks the password (and the authenticator code when two-factor authentication is on).
For the next 10 minutes this session may change two-factor settings. Other sensitive
actions answer 403 with code step_up_required until this is done.
| password required | string <= 256 characters |
| code | string <= 10 characters Authenticator code, required when two-factor authentication is on. |
{- "password": "string",
- "code": "string"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| name required | string <= 200 characters |
{- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "user@example.com",
- "name": "string",
- "email_verified": true,
- "two_factor_enabled": true,
- "created_at": "2019-08-24T14:15:22Z"
}Needs a recent password check (POST /v1/auth/step-up). The account is anonymized at
once and signed out everywhere; its personal vaults are purged within the hour, then
the account itself. Workspaces you own alone are deleted too; owning one with other
members is refused (workspace_owner): transfer it or delete it first. Encrypted
backups age out within 35 days.
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}A zip with account.json (profile, devices, API tokens, workspaces) and each personal vault as a folder of plain files under Vaults/.
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Requires a recent step-up (POST /v1/auth/step-up). Signs out every other device.
| password required | string [ 10 .. 256 ] characters |
{- "password": "stringstri"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Devices that have not been signed out, most recently used first. The plan limits how
many may be signed in at once; signing in on another one answers 403 with code
device_limit_reached until a device is signed out here.
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "platform": "web",
- "app_version": "string",
- "last_seen_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "active_sessions": 0,
- "current": true
}
]
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| device_id required | string <uuid> |
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "platform": "web",
- "app_version": "string",
- "last_seen_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "active_sessions": 0,
- "current": true
}Its sessions end; its access tokens stop working within 30 seconds.
| device_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Tokens that are not revoked. Token values are never shown again.
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "prefix": "string",
- "scopes": [
- "vaults:read"
], - "expires_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Requires a signed-in session with a recent step-up (POST /v1/auth/step-up); tokens
cannot create tokens. The value (gr_pat_ + 36 characters, the last 6 a checksum) is
returned only in this response. The plan limits how many tokens may be active (403
plan_limit_api_tokens). Send it as Authorization: Bearer <token>.
| name required | string [ 1 .. 100 ] characters |
| scopes required | Array of strings (APITokenScope) non-empty Items Enum: "vaults:read" "vaults:write" "sync" "kb:query" |
| expires_in_days | integer [ 1 .. 3650 ] Omitted means the token does not expire. |
{- "name": "string",
- "scopes": [
- "vaults:read"
], - "expires_in_days": 1
}{- "token": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "prefix": "string",
- "scopes": [
- "vaults:read"
], - "expires_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}, - "value": "string"
}The token stops working at once.
| token_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}The single source of plan limits for clients (PLAN.md 1.2). Null means unlimited.
{- "plan": {
- "key": "free",
- "name": "string"
}, - "limits": {
- "synced_vaults": 0,
- "storage_bytes": 0,
- "max_file_bytes": 0,
- "synced_devices": 0,
- "version_history_days": 0,
- "remote_mcp": true,
- "api_tokens": 0,
- "knowledge_base": true,
- "live_coediting": "own_devices",
- "drive_references": "none",
- "guests_per_vault": 0,
- "published_notes": 0,
- "workspaces": true,
- "sso": true,
- "audit_log": true,
- "webhooks": true
}, - "usage": {
- "synced_vaults": 0,
- "storage_bytes": 0
}
}Which features are turned on for the signed-in user (PLAN.md Appendix G.2). Apps read this after sign-in and hide features whose flag is off or missing. A flag stays on or off for a user while its rollout grows.
{- "flags": {
- "editor.new_toolbar": true
}
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Turns two-factor authentication on and returns recovery codes, shown only once.
| code required | string <= 10 characters |
{- "code": "string"
}{- "recovery_codes": [
- "string"
]
}Saves this device's APNs or FCM token (the mobile apps, PLAN.md P5-T06). The device gets alerts for mentions and shares, and silent "sync now" nudges when a vault it can use changes elsewhere. Only a signed-in app can do this, not an API token: 403. A token moves to the device that saved it last.
| provider required | string Enum: "apns" "fcm" apns on iPhone and iPad, fcm on Android. |
| token required | string [ 1 .. 4096 ] characters |
| sandbox | boolean Default: false An APNs token from a development build (Apple's sandbox gateway). |
{- "provider": "apns",
- "token": "string",
- "sandbox": false
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| cursor | string <= 512 characters Opaque cursor from a previous page's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
| trash | boolean Default: false List deleted vaults that can still be restored instead of active ones. |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "deleted_at": "2019-08-24T14:15:22Z",
- "purge_after": "2019-08-24T14:15:22Z"
}
], - "next_cursor": "string"
}| name required | string [ 1 .. 100 ] characters |
| workspace_id | string <uuid> Create a shared vault in this workspace (members and above) instead of a personal one. |
{- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "deleted_at": "2019-08-24T14:15:22Z",
- "purge_after": "2019-08-24T14:15:22Z"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "deleted_at": "2019-08-24T14:15:22Z",
- "purge_after": "2019-08-24T14:15:22Z"
}| vault_id required | string <uuid> |
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "deleted_at": "2019-08-24T14:15:22Z",
- "purge_after": "2019-08-24T14:15:22Z"
}The vault can be restored for 30 days (purge_after), then it is deleted permanently.
| vault_id required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "deleted_at": "2019-08-24T14:15:22Z",
- "purge_after": "2019-08-24T14:15:22Z"
}Every file you can read, at its path, exactly as synced (PLAN.md 5, Portability).
| vault_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| vault_id required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "deleted_at": "2019-08-24T14:15:22Z",
- "purge_after": "2019-08-24T14:15:22Z"
}Vault admins.
| vault_id required | string <uuid> |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "principal_type": "user",
- "principal_id": "b0fe3bdd-8201-445a-aab3-a018692d6aca",
- "access": "read",
- "folder": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Vault admins of workspace vaults. With folder, access applies to that folder only. Granting the same person or group and folder again changes the access. Guests can only read and comment.
| vault_id required | string <uuid> |
| principal_type required | string Enum: "user" "group" |
| principal_id required | string <uuid> |
| access required | string Enum: "read" "comment" "write" "admin" |
| folder | string <= 1024 characters |
{- "principal_type": "user",
- "principal_id": "b0fe3bdd-8201-445a-aab3-a018692d6aca",
- "access": "read",
- "folder": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "principal_type": "user",
- "principal_id": "b0fe3bdd-8201-445a-aab3-a018692d6aca",
- "access": "read",
- "folder": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Vault admins. Applies from the next request.
| vault_id required | string <uuid> |
| grant_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Live files as of at (default: the current head). Pass the same at with cursor to get
later pages of the same snapshot (Appendix B.3).
| vault_id required | string <uuid> |
| at | integer <int64> >= 0 |
| cursor | string <= 512 characters Opaque cursor from a previous page's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
{- "head_seq": 0,
- "files": [
- {
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "path": "string",
- "blob_hash": "string",
- "size": 0,
- "content_type": "string",
- "kind": "markdown",
- "client_mtime": "2019-08-24T14:15:22Z"
}
], - "next_cursor": "string"
}Answers 410 with code cursor_expired when since is older than the kept log.
| vault_id required | string <uuid> |
| since required | integer <int64> >= 0 |
| limit | integer [ 1 .. 1000 ] Default: 500 |
{- "changes": [
- {
- "seq": 0,
- "op": "put",
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "path": "string",
- "old_path": "string",
- "blob_hash": "string",
- "size": 0,
- "content_type": "string",
- "kind": "markdown",
- "deleted": true,
- "author_id": "78424c75-5c41-4b25-9735-3c9f7d05c59e",
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "committed_at": "2019-08-24T14:15:22Z",
- "client_mtime": "2019-08-24T14:15:22Z"
}
], - "head_seq": 0,
- "has_more": true
}| vault_id required | string <uuid> |
| Idempotency-Key | string [ 1 .. 255 ] characters ^[\x21-\x7E]+$ Makes a non-idempotent POST safe to retry. See the conventions above. |
| device_id | string <uuid> |
required | Array of objects (CommitOp) <= 1000 items |
{- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "ops": [
- {
- "type": "put",
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "base_version_id": "330601a7-a4c3-4e92-beac-4d56d32a4b5d",
- "path": "string",
- "new_path": "string",
- "blob_hash": "string",
- "size": 0,
- "content_type": "string",
- "client_mtime": "2019-08-24T14:15:22Z"
}
]
}{- "head_seq": 0,
- "results": [
- {
- "op_index": 0,
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "seq": 0
}
]
}| vault_id required | string <uuid> |
| hashes required | Array of strings <= 1000 items [ items^[0-9a-f]{64}$ ] |
{- "hashes": [
- "string"
]
}{- "missing": [
- "string"
]
}The server checks the bytes against the hash and the plan's limits. Larger blobs use upload sessions.
| vault_id required | string <uuid> |
| hash required | string^[0-9a-f]{64}$ |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| vault_id required | string <uuid> |
| hash required | string^[0-9a-f]{64}$ |
| Range | string <= 100 characters One range, |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| vault_id required | string <uuid> |
| hash required | string^[0-9a-f]{64}$ |
| size required | integer <int64> >= 0 |
{- "hash": "string",
- "size": 0
}{- "upload_id": "string",
- "hash": "string",
- "size": 0,
- "received": 0
}Upload-Offset must equal the bytes received so far (code upload_offset_mismatch otherwise).
| vault_id required | string <uuid> |
| upload_id required | string^[0-9a-f]{32}$ |
| Upload-Offset required | integer <int64> >= 0 |
{- "upload_id": "string",
- "hash": "string",
- "size": 0,
- "received": 0
}Checks the bytes against the hash and stores the blob.
| vault_id required | string <uuid> |
| upload_id required | string^[0-9a-f]{32}$ |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Superseded versions are kept for the plan's history period (PLAN.md 1.2).
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| before | integer <int64> >= 1 Return versions with a lower seq (from |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
{- "versions": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "seq": 0,
- "op": "put",
- "path": "string",
- "blob_hash": "string",
- "size": 0,
- "content_type": "string",
- "deleted": true,
- "author_id": "78424c75-5c41-4b25-9735-3c9f7d05c59e",
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "next_before": 0
}Download its content with GET /v1/vaults/{vault_id}/blobs/{blob_hash}.
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| version_id required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "seq": 0,
- "op": "put",
- "path": "string",
- "blob_hash": "string",
- "size": 0,
- "content_type": "string",
- "deleted": true,
- "author_id": "78424c75-5c41-4b25-9735-3c9f7d05c59e",
- "device_id": "3bafab7b-4400-4bcf-8e6e-09f954699940",
- "created_at": "2019-08-24T14:15:22Z"
}Commits a new version with the old content at the file's current path (a deleted file comes back).
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| version_id required | string <uuid> |
{- "head_seq": 0,
- "results": [
- {
- "op_index": 0,
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "seq": 0
}
]
}Read and change single notes by their text, for scripts and integrations. Each change is an ordinary commit that synced devices, history, and search see.
Live files in path order (all kinds; GET on a non-text file answers not_a_note).
folder limits the list to one folder and its subfolders. Needs vaults:read.
| vault_id required | string <uuid> |
| folder | string <= 1024 characters |
| cursor | string <= 512 characters Opaque cursor from a previous page's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}
], - "next_cursor": "string"
}Creates a text file (.md, .txt, …) of at most 5 MB. A live file at the same path
(compared without case) answers 409 path_taken. Needs vaults:write or sync.
| vault_id required | string <uuid> |
| path required | string [ 1 .. 1024 ] characters |
| content required | string |
{- "path": "string",
- "content": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}| vault_id required | string <uuid> |
| path required | string [ 1 .. 1024 ] characters |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}| vault_id required | string <uuid> |
| file_id required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}With base_version_id, answers 409 version_mismatch if the note changed since that
version; without it, the last write wins.
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| content required | string |
| base_version_id | string <uuid> |
{- "content": "string",
- "base_version_id": "330601a7-a4c3-4e92-beac-4d56d32a4b5d"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}Applies find/replace (the first match, or every match with all), then prepend
and append. Without base_version_id the edit is applied to the newest text, so
concurrent edits from synced devices are kept. A missing find answers 400
find_not_found.
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| find | string non-empty |
| replace | string |
| all | boolean Default: false |
| prepend | string |
| append | string |
| base_version_id | string <uuid> |
{- "find": "string",
- "replace": "string",
- "all": false,
- "prepend": "string",
- "append": "string",
- "base_version_id": "330601a7-a4c3-4e92-beac-4d56d32a4b5d"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}The note stays in version history and can be restored from there.
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| base_version_id | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Links to the note are not rewritten. A taken path answers 409 path_taken.
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| path required | string [ 1 .. 1024 ] characters |
| base_version_id | string <uuid> |
{- "path": "string",
- "base_version_id": "330601a7-a4c3-4e92-beac-4d56d32a4b5d"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "path": "string",
- "version_id": "9e94c502-ca41-4342-a7f7-af96b444512c",
- "size": 0,
- "updated_at": "2019-08-24T14:15:22Z",
- "content": "string"
}Signed HTTP callbacks for vault events (Business plan). Granite posts a JSON
WebhookEvent to your URL within seconds of each change. Answer with any 2xx status
within 10 seconds; other answers and timeouts are retried after 1 min, 5 min, 30 min,
2 h, 6 h, and 24 h, then the delivery is marked failed (410 Gone stops retries at
once). Every attempt is in the delivery log and any delivery can be replayed. Delivery
is at least once: skip event ids you have already handled.
Events: note.created, note.updated, note.moved, note.deleted; vault.shared
and member.added/member.removed/member.role_changed start with sharing.
Subscribe with exact names, note.*, member.*, or *.
Headers: Granite-Event, Granite-Delivery, Granite-Webhook, and
Granite-Signature: t=<unix seconds>,v1=<signature>, where the signature is the hex
HMAC-SHA256 of <t>.<raw body> keyed with the webhook secret (whsec_...). Check it
against the raw body before parsing, and reject timestamps more than 5 minutes old.
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const want = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1 ?? ""));
}
Python:
import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
want = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))
Go:
func verify(secret, header string, body []byte) bool {
var t, sig string
for _, p := range strings.Split(header, ",") {
k, v, _ := strings.Cut(p, "=")
if k == "t" { t = v } else if k == "v1" { sig = v }
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || math.Abs(time.Since(time.Unix(ts, 0)).Seconds()) > 300 {
return false
}
m := hmac.New(sha256.New, []byte(secret))
m.Write([]byte(t + "."))
m.Write(body)
return hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(sig))
}
Needs administrator access to the vault.
| vault_id required | string <uuid> |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "url": "string",
- "description": "string",
- "events": [
- "*"
], - "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Business plan (403 plan_limit_webhooks otherwise); at most 10 per vault. Events from
now on are delivered. The secret is returned only here and when rotated.
| vault_id required | string <uuid> |
| url required | string <= 2000 characters An https URL on the public internet. |
| description | string <= 200 characters |
| events required | Array of strings (WebhookEventType) non-empty Items Enum: "*" "note.*" "member.*" "note.created" "note.updated" "note.moved" "note.deleted" "vault.shared" "member.added" "member.removed" "member.role_changed" |
{- "url": "string",
- "description": "string",
- "events": [
- "*"
]
}{- "webhook": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "url": "string",
- "description": "string",
- "events": [
- "*"
], - "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "secret": "string"
}| vault_id required | string <uuid> |
| webhook_id required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "url": "string",
- "description": "string",
- "events": [
- "*"
], - "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Omitted fields keep their value. rotate_secret returns a new secret once.
| vault_id required | string <uuid> |
| webhook_id required | string <uuid> |
| url | string <= 2000 characters |
| description | string <= 200 characters |
| events | Array of strings (WebhookEventType) non-empty Items Enum: "*" "note.*" "member.*" "note.created" "note.updated" "note.moved" "note.deleted" "vault.shared" "member.added" "member.removed" "member.role_changed" |
| active | boolean |
| rotate_secret | boolean |
{- "url": "string",
- "description": "string",
- "events": [
- "*"
], - "active": true,
- "rotate_secret": true
}{- "webhook": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "url": "string",
- "description": "string",
- "events": [
- "*"
], - "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "secret": "string"
}Its pending deliveries and delivery log are deleted too.
| vault_id required | string <uuid> |
| webhook_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Deliveries, newest first, with the outcome of their latest attempt.
| vault_id required | string <uuid> |
| webhook_id required | string <uuid> |
| cursor | string <= 512 characters Opaque cursor from a previous page's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "event_type": "string",
- "state": "pending",
- "attempts": 0,
- "next_attempt_at": "2019-08-24T14:15:22Z",
- "last_status": 0,
- "last_error": "string",
- "replay_of": "eb051ed0-925f-449a-a6c5-0c118d499f91",
- "created_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "payload": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "data": { }
}
}
], - "next_cursor": "string"
}Creates a new delivery of the same event (same event id), sent right away.
| vault_id required | string <uuid> |
| webhook_id required | string <uuid> |
| delivery_id required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "event_type": "string",
- "state": "pending",
- "attempts": 0,
- "next_attempt_at": "2019-08-24T14:15:22Z",
- "last_status": 0,
- "last_error": "string",
- "replay_of": "eb051ed0-925f-449a-a6c5-0c118d499f91",
- "created_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "payload": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "data": { }
}
}The consent page and connected apps of Granite's OAuth 2.1 server. The protocol endpoints
themselves (/.well-known/oauth-authorization-server, /oauth/register,
/oauth/authorize, /oauth/token, /oauth/revoke) follow the OAuth RFCs and are not
part of this API.
For the consent page: validates the request an app sent to /oauth/authorize and
describes the app. When the request is invalid but its redirect URI is the app's,
error_redirect is where to send the browser.
| client_id required | string <= 200 characters |
| redirect_uri | string <= 2000 characters |
| response_type | string <= 50 characters |
| scope | string <= 500 characters |
| state | string <= 2000 characters |
| code_challenge | string <= 200 characters |
| code_challenge_method | string <= 20 characters |
| resource | string <= 2000 characters |
{- "client_id": "string",
- "client_name": "string",
- "client_uri": "string",
- "logo_uri": "string",
- "redirect_host": "string",
- "scopes": [
- "vaults:read"
], - "error_redirect": "string"
}Records the decision. Allowing creates (or updates) the app's grant with the chosen
permissions and vaults (empty vault_ids means all vaults). The browser goes to
redirect_to either way.
required | object (OAuthAuthorizeRequest) |
| approve required | boolean |
| scopes | Array of strings (OAuthScope) Items Enum: "vaults:read" "vaults:write" "kb:query" |
| vault_ids | Array of strings <uuid> <= 100 items [ items <uuid > ] |
{- "request": {
- "client_id": "string",
- "redirect_uri": "string",
- "response_type": "string",
- "scope": "string",
- "state": "string",
- "code_challenge": "string",
- "code_challenge_method": "string",
- "resource": "string"
}, - "approve": true,
- "scopes": [
- "vaults:read"
], - "vault_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}{- "redirect_to": "string"
}{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "client_id": "string",
- "client_name": "string",
- "client_uri": "string",
- "scopes": [
- "vaults:read"
], - "vault_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "created_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z"
}
]
}Its access ends at once; it has to ask again.
| grant_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Company workspaces (Business plan): members with roles (owner, admin, member, guest), invitations by email or link, and verified email domains that let colleagues join. Anyone who is not a member gets 404 for everything about a workspace.
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "role": "owner",
- "domain_capture": true,
- "policy_block": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}You become its owner. Needs the Business plan (403 plan_limit_workspaces).
| name required | string [ 1 .. 100 ] characters |
| slug | string^[a-z0-9][a-z0-9-]{1,47}$ |
{- "name": "string",
- "slug": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "role": "owner",
- "domain_capture": true,
- "policy_block": "string",
- "created_at": "2019-08-24T14:15:22Z"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "role": "owner",
- "domain_capture": true,
- "policy_block": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Admins.
| workspace_id required | string <uuid> |
| name | string [ 1 .. 100 ] characters |
| domain_capture | boolean |
{- "name": "string",
- "domain_capture": true
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "role": "owner",
- "domain_capture": true,
- "policy_block": "string",
- "created_at": "2019-08-24T14:15:22Z"
}The owner. Its vaults and keys are destroyed after the recovery window.
| workspace_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Members and above.
| workspace_id required | string <uuid> |
{- "items": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "email": "string",
- "name": "string",
- "role": "owner",
- "status": "active",
- "joined_at": "2019-08-24T14:15:22Z"
}
]
}Admins manage members and guests; only the owner manages admins.
| workspace_id required | string <uuid> |
| user_id required | string <uuid> |
| role required | string Enum: "admin" "member" "guest" |
{- "role": "admin"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins (only the owner removes admins). Removing yourself is leaving.
| workspace_id required | string <uuid> |
| user_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}The owner; they become an admin.
| workspace_id required | string <uuid> |
| user_id required | string <uuid> |
{- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}The owner must transfer ownership first.
| workspace_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}For workspaces in GET /v1/me/joinable-workspaces.
| workspace_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins.
| workspace_id required | string <uuid> |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "string",
- "role": "admin",
- "expires_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "link": "string"
}
]
}Admins. With email, sends an invitation for that address (single use, 7 days); without, returns a link anyone signed in can use until it expires (30 days) or is revoked. Only the owner invites admins.
| workspace_id required | string <uuid> |
string <= 254 characters | |
| role required | string Enum: "admin" "member" "guest" |
{- "email": "string",
- "role": "admin"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "email": "string",
- "role": "admin",
- "expires_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "link": "string"
}Admins.
| workspace_id required | string <uuid> |
| invitation_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins. Add the returned TXT record to the domain's DNS, then verify.
| workspace_id required | string <uuid> |
| domain required | string <= 253 characters |
{- "domain": "string"
}{- "domain": "string",
- "txt_record": "string",
- "verified_at": "2019-08-24T14:15:22Z"
}Admins.
| workspace_id required | string <uuid> |
| domain required | string <= 253 characters |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins. Answers 409 domain_not_verified until the TXT record is visible.
| workspace_id required | string <uuid> |
| domain required | string <= 253 characters |
{- "domain": "string",
- "txt_record": "string",
- "verified_at": "2019-08-24T14:15:22Z"
}Does not need sign-in. The token is sent in the body so it stays out of logs.
| token required | string <= 200 characters |
{- "token": "string"
}{- "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
- "workspace_name": "string",
- "role": "string",
- "email": "string"
}Email invitations work only for the invited, verified address.
| token required | string <= 200 characters |
{- "token": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "slug": "string",
- "role": "owner",
- "domain_capture": true,
- "policy_block": "string",
- "created_at": "2019-08-24T14:15:22Z"
}Admins.
| workspace_id required | string <uuid> |
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "members": 0
}Admins.
| workspace_id required | string <uuid> |
| group_id required | string <uuid> |
| name required | string [ 1 .. 100 ] characters |
{- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "members": 0
}Admins. Its grants stop applying.
| workspace_id required | string <uuid> |
| group_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Members and above.
| workspace_id required | string <uuid> |
| group_id required | string <uuid> |
{- "items": [
- {
- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",
- "email": "string",
- "name": "string"
}
]
}Admins. Takes effect on the member's next request.
| workspace_id required | string <uuid> |
| group_id required | string <uuid> |
| user_id required | string <uuid> |
{- "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins.
| workspace_id required | string <uuid> |
| group_id required | string <uuid> |
| user_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins. Newest first. Filters combine.
| workspace_id required | string <uuid> |
| cursor | string <= 512 characters Opaque cursor from a previous page's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
| action | string <= 100 characters Actions starting with this, such as member. or vault.shared |
| actor | string <uuid> |
| since | string <date-time> |
| until | string <date-time> |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "at": "2019-08-24T14:15:22Z",
- "actor_id": "04f37679-bfbf-4906-b749-01756515cecf",
- "actor_email": "string",
- "via": "string",
- "action": "string",
- "target_type": "string",
- "target_id": "d3bcdc92-4191-401b-ad0c-42056c6efab9",
- "ip": "string",
- "user_agent": "string",
- "metadata": { }
}
], - "next_cursor": "string"
}Admins. CSV, or JSON with one event per line. Up to 100,000 events, newest first.
| workspace_id required | string <uuid> |
| format required | string Enum: "csv" "json" |
| action | string <= 100 characters |
| since | string <date-time> |
| until | string <date-time> |
Admins. 404 when SSO is not set up.
| workspace_id required | string <uuid> |
{- "protocol": "oidc",
- "issuer": "string",
- "client_id": "string",
- "saml": {
- "entity_id": "string",
- "acs_url": "string",
- "metadata_url": "string"
}, - "jit_role": "member",
- "enforced": true,
- "tested_at": "2019-08-24T14:15:22Z",
- "redirect_url": "string"
}Admins. For OpenID Connect, register redirect_url with the provider. For SAML, give
the identity provider's metadata (idp_metadata XML or idp_metadata_url) and register
Granite's service provider with it: saml.metadata_url serves Granite's metadata
(GET /v1/sso/saml/{workspace_id}/metadata); the provider posts responses to
saml.acs_url (POST /v1/sso/saml/{workspace_id}/acs). Responses must be signed (or
carry signed assertions); replayed assertions are refused; sign-in can start at Granite
or at the provider. People at the workspace's verified
domains can then sign in with SSO. enforced needs one successful SSO sign-in with the
current issuer and client first; then members at those domains (all but the owner)
must use SSO, and their other sessions cannot open the workspace's vaults.
| workspace_id required | string <uuid> |
| protocol | string Default: "oidc" Enum: "oidc" "saml" |
| issuer | string <= 500 characters OpenID Connect. |
| client_id | string <= 500 characters |
| idp_metadata | string <= 1048576 characters SAML identity provider metadata XML (leave out to keep the stored one). |
| idp_metadata_url | string <= 2000 characters Where to fetch the SAML identity provider metadata instead. |
| client_secret | string <= 2000 characters Required the first time; leave out to keep the stored secret. |
| jit_role | string Enum: "member" "guest" |
| enforced required | boolean |
{- "protocol": "oidc",
- "issuer": "string",
- "client_id": "string",
- "idp_metadata": "string",
- "idp_metadata_url": "string",
- "client_secret": "string",
- "jit_role": "member",
- "enforced": true
}{- "protocol": "oidc",
- "issuer": "string",
- "client_id": "string",
- "saml": {
- "entity_id": "string",
- "acs_url": "string",
- "metadata_url": "string"
}, - "jit_role": "member",
- "enforced": true,
- "tested_at": "2019-08-24T14:15:22Z",
- "redirect_url": "string"
}| workspace_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins. SCIM 2.0 (RFC 7644) lives at base_url (/scim/v2): Users are the workspace's
members (emails at verified domains), Groups its groups. Deactivating or deleting a user
suspends or removes the membership and revokes the account's sessions, API tokens, and
OAuth grants at once. The owner cannot be deprovisioned through SCIM.
| workspace_id required | string <uuid> |
{- "enabled": true,
- "base_url": "string",
- "token_prefix": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z"
}| workspace_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Admins. The policy applies to the workspace's vaults at once: a refused request gets
403 with a policy_... error code (policy_two_factor_required,
policy_session_expired, policy_client_not_allowed, policy_mcp_off,
policy_mcp_read_only, policy_knowledge_base_off). Requiring two-step verification
needs it on for the admin first.
| workspace_id required | string <uuid> |
| require_two_factor required | boolean Members need two-step verification to use the workspace's vaults. |
| session_hours required | integer [ 0 .. 8760 ] How long after signing in a session may use the workspace's vaults (0 for no limit). |
| clients required | Array of strings non-empty Items Enum: "web" "desktop" "mobile" "api" Apps allowed to use the workspace's vaults ( |
| mcp required | string Enum: "on" "read_only" "off" What AI tools connected through MCP may do. |
| knowledge_base required | boolean |
| drive required | boolean Google Drive references (when available). |
{- "require_two_factor": true,
- "session_hours": 0,
- "clients": [
- "web"
], - "mcp": "on",
- "knowledge_base": true,
- "drive": true
}{- "require_two_factor": true,
- "session_hours": 0,
- "clients": [
- "web"
], - "mcp": "on",
- "knowledge_base": true,
- "drive": true
}Admins. New events are POSTed in batches ({"workspace_id", "events": [...]}) within a
minute, signed like webhooks (Granite-Signature). The secret is returned only here.
| workspace_id required | string <uuid> |
| url required | string <= 2000 characters |
{- "url": "string"
}{- "url": "string",
- "secret": "string",
- "last_error": "string"
}| workspace_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Comments on notes, kept outside the Markdown file. A thread's first comment is anchored to a quote of the text with a little context before and after; clients find the quote again after edits. Mentioned people who can read the note are notified.
Oldest first; replies have parent_id.
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "parent_id": "1c6ca187-e61f-4301-8dcb-0e9749e89eef",
- "author_id": "78424c75-5c41-4b25-9735-3c9f7d05c59e",
- "author_email": "string",
- "author_name": "string",
- "body": "string",
- "anchor": {
- "quote": "string",
- "prefix": "string",
- "suffix": "string",
- "line": 0
}, - "resolved_at": "2019-08-24T14:15:22Z",
- "edited_at": "2019-08-24T14:15:22Z",
- "deleted": true,
- "created_at": "2019-08-24T14:15:22Z"
}
]
}Needs comment access to the note. mentions lists people to notify (those who cannot read the note are skipped).
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| body required | string [ 1 .. 10000 ] characters |
object (CommentAnchor) | |
| parent_id | string <uuid> |
| mentions | Array of strings <uuid> <= 50 items [ items <uuid > ] |
{- "body": "string",
- "anchor": {
- "quote": "string",
- "prefix": "string",
- "suffix": "string",
- "line": 0
}, - "parent_id": "1c6ca187-e61f-4301-8dcb-0e9749e89eef",
- "mentions": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "parent_id": "1c6ca187-e61f-4301-8dcb-0e9749e89eef",
- "author_id": "78424c75-5c41-4b25-9735-3c9f7d05c59e",
- "author_email": "string",
- "author_name": "string",
- "body": "string",
- "anchor": {
- "quote": "string",
- "prefix": "string",
- "suffix": "string",
- "line": 0
}, - "resolved_at": "2019-08-24T14:15:22Z",
- "edited_at": "2019-08-24T14:15:22Z",
- "deleted": true,
- "created_at": "2019-08-24T14:15:22Z"
}Only the author edits body; anyone who may comment resolves a thread.
| vault_id required | string <uuid> |
| comment_id required | string <uuid> |
| body | string [ 1 .. 10000 ] characters |
| resolved | boolean |
{- "body": "string",
- "resolved": true
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}The author or a vault admin. Replies stay.
| vault_id required | string <uuid> |
| comment_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}| cursor | string <= 512 characters Opaque cursor from a previous page's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Maximum items to return. |
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "kind": "mention",
- "actor_id": "04f37679-bfbf-4906-b749-01756515cecf",
- "actor_email": "string",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "vault_name": "string",
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "path": "string",
- "comment_id": "24dbb54d-334d-4b2a-92ae-e15845e5d822",
- "read_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "unread": 0,
- "next_cursor": "string"
}The listed ones, or all when ids is empty.
| ids | Array of strings <uuid> <= 200 items [ items <uuid > ] |
{- "ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}Searches the notes of one vault the caller may read. Query syntax: words match by
prefix and all must match; "a phrase"; -word excludes; tag:x (also finds #x/y),
path:x, and file:x filter. Matching ignores case and accents. Notes are indexed a few
seconds after each commit; pending counts notes not indexed yet.
| vault_id required | string <uuid> |
| q required | string [ 1 .. 500 ] characters |
| limit | integer [ 1 .. 50 ] Default: 20 |
{- "results": [
- {
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "path": "string",
- "title": "string",
- "snippet": "string",
- "line": 0,
- "score": 0.1
}
], - "pending": 0
}Retrieval for AI tools (Pro and Business). Granite has no built-in assistant: tools search the knowledge base and get ranked passages with citations, then answer with their own model. Only vaults the caller may read are searched.
Vault admins. Applies at once: an excluded folder's passages are removed from the index in this request, and files that come back in are queued for indexing.
| vault_id required | string <uuid> |
| included required | boolean The vault is in the knowledge base (when the plan includes it). |
| excluded_folders required | Array of strings <= 100 items Folders (with everything inside) left out of the knowledge base. |
| exclude_drive required | boolean Leave Google Drive references out. |
{- "included": true,
- "excluded_folders": [
- "string"
], - "exclude_drive": true
}{- "included": true,
- "excluded_folders": [
- "string"
], - "exclude_drive": true
}Vault admins.
| vault_id required | string <uuid> |
{- "available": true,
- "indexed": true,
- "documents": 0,
- "chunks": 0,
- "pending": 0,
- "last_indexed_at": "2019-08-24T14:15:22Z",
- "failures": [
- {
- "path": "string",
- "error": "string"
}
]
}Ranks note and document passages (chunks split by headings, pages of PDFs) by full-text
relevance and, when semantic search is enabled, meaning, fused by reciprocal rank.
Searches every vault the caller may read, or only vault_ids. Needs a plan with the
knowledge base (403 plan_limit_knowledge_base); tokens need the kb:query scope.
60 searches a minute per caller (429 kb_rate_limited). Files are indexed within a
minute of a change; pending counts files waiting.
| query required | string [ 1 .. 2000 ] characters |
| vault_ids | Array of strings <uuid> <= 100 items [ items <uuid > ] |
| limit | integer [ 1 .. 50 ] Default: 10 |
{- "query": "string",
- "vault_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "limit": 10
}{- "passages": [
- {
- "chunk_id": "55e808e1-8ddc-49f1-92c9-6bbdcdff1c83",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "path": "string",
- "title": "string",
- "heading_path": [
- "string"
], - "start_line": 0,
- "end_line": 0,
- "page": 0,
- "text": "string",
- "score": 0.1
}
], - "pending": 0
}Makes the note readable by anyone with the link at /p/{slug} (rendered on the server
from the note's current version and sanitized). Calling it again changes the address or
password. Needs vault admin access; the plan limits how many notes are published
(plan_limit_published_notes).
| vault_id required | string <uuid> |
| file_id required | string <uuid> |
| slug | string^[a-z0-9][a-z0-9-]{2,63}$ The page address; omitted keeps the current one or picks a random one. |
| password | string <= 256 characters Omitted keeps the current password; "" removes it. |
{- "slug": "string",
- "password": "string"
}{- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "path": "string",
- "slug": "string",
- "protected": true
}| vault_id required | string <uuid> |
| file_id required | string <uuid> |
{- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "code": "string",
- "request_id": "string",
- "errors": [
- {
- "field": "string",
- "detail": "string"
}
]
}