API overview & authentication
The SafeHanded REST API lets you create requests, read their state, end them, and feed ticket status changes in from your own tooling. For: Integrators. Available on Team and above.
- Base URL:
https://safehanded.com/api/v1 - API reference — every endpoint, with parameters, request bodies, example responses, ready-to-copy code (curl, JavaScript, Python, PHP and more) and a built-in client to try calls with your own key.
- Postman collection and OpenAPI 3.1 document — see Try it in Postman.
Authentication
Section titled “Authentication”Create a key under Settings → API (Owner/Admin). Keys look like sh_live_ followed by 43
characters; SafeHanded stores only a hash and shows the key once. Send it as a bearer token:
Authorization: Bearer sh_live_…Each key carries:
| Property | Meaning |
|---|---|
| Scopes | requests:read, requests:write, tickets:write, events:read, webhooks:write. A missing scope answers 403. |
| Client scope (MSP) | Optional — a client-scoped key only sees and creates for that client. |
| IP allowlist | Optional addresses or CIDR blocks; use from elsewhere answers 403 and is audited. |
| Expiry | Optional; expired or revoked keys answer 401. |
Test a key with GET /ping — it returns the organisation, the key’s scopes and a connection label.
Try it in Postman
Section titled “Try it in Postman”- Download the SafeHanded Postman collection (right-click
→ Save link as…), then in Postman choose Import and drop the file in.
Or choose Import → Link and paste
https://docs.safehanded.com/safehanded-api.postman_collection.json. - Open the SafeHanded API collection → Variables and paste your key into
apiKey. Every request uses it automatically. - Send Connection → Check the API key. A
200with your organisation’s name means you’re set up.
The requests are grouped like the reference (Connection, Requests, Shares, Events, Tickets, Webhook
subscriptions) and
come with example bodies. POSTs send a fresh Idempotency-Key each time, so a resend creates a new
request rather than replaying the last one.
Prefer to generate your own client, or use Insomnia, Bruno or another tool? Import the
OpenAPI 3.1 document instead — Postman accepts it too (Import → Link,
https://docs.safehanded.com/openapi.json).
Conventions
Section titled “Conventions”- Errors are
{ "message", "code" }; codes includeunauthenticated,forbidden,not_found,validation_failed,conflict,rate_limited, andplan_required(403) when your plan doesn’t include the API. - Rate limit 600 requests/minute per key, and 1,200 requests/minute across all of an organisation’s
keys (
X-RateLimit-*;429withRetry-After). - Idempotency — send
Idempotency-Key: <unique string>with a POST; the first response is stored for 24 hours and replayed for the same key and body. A replay returns the same status andLocationheader, but secret fields (link,link_without_fragment,challenge_code, a share’ssecret/value) come backnull: they are shown once, on the original response. - Pagination —
limit(≤ 100) andcursor(the previous page’snext_cursor); newest first. - Ids are 26-character ULIDs; times are ISO-8601.
Core endpoints
Section titled “Core endpoints”| Method and path | Scope | Purpose |
|---|---|---|
GET /ping | any | Verify the key |
POST /generate | any | Generate a strong password/passphrase (stores nothing) |
POST /requests | requests:write | Create and commit a request; returns the full link |
GET /requests | requests:read | List (filter by status, client_id, ticket_ref) |
GET /requests/{id} | requests:read | One request |
POST /requests/{id}/revoke | requests:write | Kill a live link |
POST /requests/{id}/complete | requests:write | Destroy a submitted credential |
POST /tickets/{ref}/status | tickets:write | Mark a ticket status (close-to-purge) |
GET /events | events:read | Poll event objects |
GET/POST/DELETE /webhooks | webhooks:write | Manage webhook subscriptions |
The API reference has the full detail for each, including the certificate downloads.
Create a request
Section titled “Create a request”curl -X POST https://safehanded.com/api/v1/requests \ -H "Authorization: Bearer sh_live_…" -H "Content-Type: application/json" \ -H "Idempotency-Key: ticket-4411-attempt-1" \ -d '{ "subject_name": "Jane Doe", "subject_email": "jane@customer.example", "justification": "Password reset for ticket 4411, identity confirmed by phone.", "recipients": ["tech@msp.example"], "ticket_ref": "HD-4411", "ttl_minutes": 60, "verification_mode": "challenge", "delivery": { "method": "email" } }'Recipients are members of your organisation, addressed by email; each must have set up an identity key
(the request is encrypted to them). The 201 response contains the request, the complete link, and
the challenge_code when the verification mode needs one — give the code to the subject out of band,
never in the same message as the link.
Close-to-purge from any platform
Section titled “Close-to-purge from any platform”POST /api/v1/tickets/HD-4411/status{ "status": "closed" }Where the linked adapter can read the ticket back, the status is verified against the ticket system before anything is destroyed. Requests that merely quote the ticket reference are matched too, but those are unverified and only purge when Trust unverified webhooks is enabled in Policy.
Security
Section titled “Security”- Prefer one key per integration with the smallest scopes, an IP allowlist, and an expiry.
- Every API action is audited with the key’s name; revoke a key and its callers get
401immediately. - Payloads never contain envelope material, link tokens, codes or key material — enforced by a test suite over every response shape.