GuidesGet started
Authentication & scopes
Every request is made with an API key. The key decides whether you’re in live or test mode, and it can only do what its scopes allow.
API keys
Create keys in Settings › Developers. The full key is shown once, when you create it — put it straight into your secret manager. After that, the key list shows only its prefix and id, like sa_live_7Gk2Pq9X….
| Key | Starts with | What it does |
|---|---|---|
| Live | sa_live_ | Real envelopes. Emails and texts go out, signatures are legally binding, and envelopes count toward your plan. |
| Test | sa_test_ | Test envelopes. No emails or texts, never billed or counted. See Test mode. |
There’s one host for both modes: the key’s prefix decides which mode a call runs in. An id from the other mode returns 404 not_found.
When you create a key you can also limit it to an IP allowlist (calls from anywhere else return 403 ip_not_allowed) and give it an expiry date. A revoked key stops working within 30 seconds. Creating and revoking keys are recorded in the account’s activity log.
Never put a key in a browser, a mobile app or a public repository. Anyone who has it can act as your account within its scopes. If a key leaks, revoke it in Settings › Developers.
Authenticating requests
Send the key as a bearer token in the Authorization header of every request, over HTTPS. The base URL is https://api.skanything.io/v1.
curl https://api.skanything.io/v1/account \ -H "Authorization: Bearer $SA_API_KEY"
const res = await fetch("https://api.skanything.io/v1/account", {
headers: { Authorization: `Bearer ${process.env.SA_API_KEY}` },
});
const account = await res.json();
if (!res.ok) throw new Error(`${account.code}: ${account.detail}`);
console.log(account.plan, account.billing_status);import os
import requests
res = requests.get(
"https://api.skanything.io/v1/account",
headers={"Authorization": f"Bearer {os.environ['SA_API_KEY']}"},
)
res.raise_for_status()
account = res.json()
print(account["plan"], account["billing_status"])using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("SA_API_KEY"));
var account = await http.GetStringAsync("https://api.skanything.io/v1/account");
Console.WriteLine(account);Never put the key in the URL. A key in the query string is refused with 400 api_key_in_query — and you should revoke it, because URLs end up in logs.
A call without a usable key gets 401: missing_api_key (no Bearer header), invalid_api_key (mistyped, cut short or unknown), api_key_revoked, api_key_expired or account_suspended.
Scopes
When you create a key, pick Full access or choose its scopes. A scope lets the key do one kind of thing, like reading envelopes or voiding them. Scopes can’t be changed later: to change what a key can do, create a new key and revoke the old one.
Scope reference
| Scope | Lets the key | Notes |
|---|---|---|
| Envelopes | ||
envelopes:read | List and see envelopes, their recipients, fields and documents, and the audit trail | |
envelopes:write | Create and change drafts, add documents, recipients and fields, send, remind, make signing URLs, delete drafts | Doesn’t include envelopes:read |
envelopes:void | Void envelopes | |
documents:download | Download documents, signed PDFs and certificates | |
| Templates | ||
templates:read | List and see templates and their data schema | |
templates:write | Preview a template as a PDF | Templates are created and edited in the app, not the API |
| Files · KeepAnything | ||
files:read | List and download files and folders, versions, the change feed and search; see grants, links and file requests | Live keys only |
files:write | Upload, rename, move, copy, trash and restore files and folders | Live keys only |
sharing:write | Share, make links and file requests, change and remove them | Live keys only |
| Account | ||
webhooks:manage | See, add, change and remove webhook endpoints, rotate their secrets, send tests, see and redeliver deliveries | |
| Full access | ||
* | Every scope, including ones we add later | Keys made before scopes existed have Full access |
How scopes combine
- A write scope doesn’t include its read scope. A key that sends envelopes and checks on them needs both
envelopes:writeandenvelopes:read. envelopes:readalso covers the audit trail.- Any key can call
GET /v1/account. No scope needed. - More scopes arrive as the API grows. Full access picks them up automatically; scoped keys don’t.
- Every endpoint in the API reference shows the scope it needs.
When a scope is missing
The call fails with 403 and the code insufficient_scope; required_scope names the scope the key needs. Checks run in this order, so you always get the first problem: key → IP allowlist → route → test mode → scope → rate limit. All errors use RFC 9457 problem details — see Errors.
POST /v1/envelopes/env_4f9c2d7e1b8a4c6f9e2d3b7a1c5e8f02/void
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
SignAnything-Request-Id: req_7c1e5a9d3f2b4e8a6c0d9f1b
{
"type": "https://api.skanything.io/v1/docs/errors#insufficient_scope",
"title": "Forbidden",
"status": 403,
"code": "insufficient_scope",
"detail": "This API key needs the envelopes:void scope.",
"request_id": "req_7c1e5a9d3f2b4e8a6c0d9f1b",
"errors": null,
"required_scope": "envelopes:void"
}Test keys
Test keys take the envelope, template and webhook scopes and see the same templates and account as live keys. They create test envelopes, which are signed and sealed exactly like live ones — but no emails or texts go out (sign through a signing URL), and nothing is billed or counted. Test mode allows 30 requests a minute.
KeepAnything has no test mode, so a test key can’t be given files:read, files:write or sharing:write. Calls to files, uploads, folders, the change feed, search, grants, links and file requests with a test key return 403 test_mode_not_supported, even with Full access.
GET /v1/folders/company/children
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
SignAnything-Request-Id: req_2b8d4f0a6c1e4a9b7d3f5c0e
{
"type": "https://api.skanything.io/v1/docs/errors#test_mode_not_supported",
"title": "Forbidden",
"status": 403,
"code": "test_mode_not_supported",
"detail": "Test keys can't read or change files: KeepAnything has no test mode. Use a live key for KeepAnything.",
"request_id": "req_2b8d4f0a6c1e4a9b7d3f5c0e",
"errors": null
}Checking a key
GET /v1/account works with any key and returns the account’s plan, billing status, seats and this period’s envelope usage. It’s a good start-up check for your integration: it proves the key works before you send anything. On plans with unlimited envelopes, included_envelopes and included_remaining_this_period are null.
{
"object": "account",
"plan": "business",
"billing_status": "active",
"seats": 12,
"current_period_start": "2026-11-01T00:00:00Z",
"current_period_end": "2026-12-01T00:00:00Z",
"cancel_at_period_end": false,
"included_envelopes": 500,
"included_used_this_period": 214,
"included_remaining_this_period": 286,
"pack_envelopes_remaining": 0
}billing_status is one of none, trialing, active, past_due, suspended or canceled; plan is none without a subscription.