Developer documentation
Integrate your systems with Grantledger in two directions: a REST API your systems call to read access and drive workflows, and webhooks Grantledger sends you when access changes. Everything is scoped to your workspace.
Authentication
Create an API key in the Grantledger app (Connectors area → API keys). The key is shown once — store it in your secrets manager. Send it as a bearer token:
Authorization: Bearer glk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Every request is automatically scoped to your workspace — a key can only ever see or
change your own data. Call the API on your workspace subdomain
(https://your-workspace.grantledger.co/api/v1) or on the
central domain (https://grantledger.co/api/v1). On a subdomain the
key must belong to that workspace — a key can never be replayed against another workspace's subdomain.
Scopes
Each key carries one or more scopes; a route requires a specific scope, and a key that lacks it gets
403.
| Scope | Grants |
|---|---|
read | List employees, access grants, access requests, audit logs. |
write | Open an access request (routed through approval). |
provision | Revoke a grant (through the connector + audit trail). |
Base URL, rate limits & errors
- Base URL:
https://your-workspace.grantledger.co/api/v1(or the centralhttps://grantledger.co/api/v1) - Rate limit: 120 requests/minute per key (
429when exceeded). - Errors return JSON
{ "error": "…" }with401(bad/missing/revoked key),403(missing scope),422(validation),429(rate limit).
Endpoints
List your employees (paginated; per_page up to 200).
Fetch one employee.
Who has access to what. Filter by employee_id, status.
Revoke a grant. Removes the external access first (if the system is connector-enforced); if that fails, the grant is left active and an error is returned.
List access requests. Filter by status.
Open a request. It is routed through the approval chain — you cannot skip approval or
set a status/approver (those are ignored). Body: requester_id,
target_employee_id, system_id, optional access_level_id,
justification, requested_duration.
The tamper-evident access audit log (newest first). Filter by action, since.
Download the OpenAPI 3 spec → (machine-readable contract for every endpoint — generate a client from it).
Example: list employees
curl -H "Authorization: Bearer $GL_KEY" \
https://grantledger.co/api/v1/employeesconst res = await fetch("https://grantledger.co/api/v1/employees", {
headers: { Authorization: `Bearer ${process.env.GL_KEY}` },
});
const { data } = await res.json();import os, requests
res = requests.get(
"https://grantledger.co/api/v1/employees",
headers={"Authorization": f"Bearer {os.environ['GL_KEY']}"},
)
data = res.json()["data"]$res = Http::withToken(getenv('GL_KEY'))
->get('https://grantledger.co/api/v1/employees');
$data = $res->json('data');Connectors
Connect an app and Grantledger will provision access on approval and revoke it on offboarding or expiry — not just track it. Connect nothing and that app stays a manual system of record. Cloud providers use secure federation (no long-lived secret stored); everything else uses a scoped token stored encrypted with your workspace's own key. You set these up in-app (each has a step-by-step guide), or use the generic REST connector for any tool with an HTTP API.
Apps — Token-based; add a vendor with no new code
- Slack (non-Grid)
- Zoom
- HubSpot
- Zendesk
- TestRail
- Bitbucket
- Freshdesk
- MongoDB Atlas
- PlanetScale
- DigitalOcean
- Asana
Cloud — Federated — no stored secret
- AWS IAM federated
- Google Cloud (GCP IAM) federated
- Microsoft Azure (RBAC) federated
- HashiCorp Vault federated
Custom — Any HTTP grant/revoke endpoint
- Custom REST endpoint
Dev tools — Org / group membership
- GitHub
- GitLab
HR — Inbound: terminations auto-start offboarding
- HRIS (BambooHR / Gusto)
Identity — One SCIM connector covers many IdPs
- SCIM (Okta, Entra, Google, Salesforce, 1Password, …)
Don't see your tool? The generic REST connector handles most HTTP APIs, and SCIM covers any SCIM 2.0 provider. Ask us about anything else.
Webhooks
Register endpoints in the Grantledger app (Webhooks page). When a subscribed event fires we POST a JSON body to your URL. Payloads carry only ids — no employee PII; correlate them against your own data.
Headers & payload
X-Grantledger-Event: granted
X-Grantledger-Signature: sha256=<hex hmac of the raw body>
{
"event": "granted",
"occurred_at": "2026-07-13T09:12:00+00:00",
"data": {
"audit_id": 8821, "subject_type": "AccessGrant", "subject_id": 501,
"employee_id": 42, "system_id": 7, "actor_id": 12
}
}
Events you can subscribe to
granted, revoked, request_opened, approved,
rejected, offboarding_initiated, offboarding_completed,
provisioning_failed, certification_completed, reconciliation_run,
consent_granted, consent_withdrawn, subject_data_exported,
subject_data_erased. (Leave the subscription empty to receive all events.)
Verifying the signature
Recompute sha256=HMAC-SHA256(rawRequestBody, yourSigningSecret) and compare it
(constant-time) with the X-Grantledger-Signature header. Reject on mismatch.
# Signature is verified in your app, not with curl. See the code tabs.
# Header sent to you: X-Grantledger-Signature: sha256=<hmac>const crypto = require("crypto");
function verify(rawBody, header, secret) {
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header || ""), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header or "")function verify(string $rawBody, string $header, string $secret): bool
{
$expected = 'sha256='.hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $header);
}Failed webhook deliveries are retried with backoff. A URL that isn't https or points at a private/internal address is refused. Questions? Contact us.