Core concepts

Authentication

Every Worksible API request is authenticated with an API key bound to one organization. Keys carry an explicit list of scopes and an optional expiry. The platform stores only a hash; the plaintext is shown to you exactly once at creation.

Available on
ProfessionalGlobalEnterpriseAOR

Read scopes are available on every paid plan. Some write scopes (payments, invoices, contracts) require Global or above; AOR customers get full read + write.

Key format

Worksible keys follow a single, scannable format. The environment is in the key itself, so accidental cross-environment use is loud rather than silent.

text
wsk_<env>_<PREFIX>_<SECRET>
   |     |        |
   |     |        └── 48-char random secret. Hashed at rest. Never recoverable.
   |     └── 8-char public id. Shown in the dashboard for identification.
   └── "live" or "test".

Anatomy

  • Prefix— the visible public id (e.g. AbCd1234) identifies the row to lookup. It's safe to log.
  • Secret— the high-entropy random suffix. We hash it on creation; the plaintext is never persisted in cleartext, in our databases, in our logs, or anywhere else. We can't show it to you twice.
Treat keys like passwords
Never log the full key, never embed it in a URL that ends up in a browser history, never commit it. If a key leaks, revoke it from Settings → API keys. The next request that reaches us gets 401 revoked.

Sending the key

The API accepts the key in three places, in this order of preference:

  1. Header (recommended): X-API-Key— a clean, dedicated header that some HTTP intermediaries treat as an opaque token (no rewriting).
  2. Bearer token: Authorization: Bearer <key>— fits OAuth-shaped HTTP clients without configuration.
  3. Query parameter: ?api_key=<key>— supported but discouraged. Query strings end up in referrer headers, server access logs and browser history. Use only for one-off testing.
header (preferred)
curl https://api.worksible.com/v1/companies/me \
  -H "X-API-Key: wsk_live_AbCd1234_xR9k..."
bearer
curl https://api.worksible.com/v1/companies/me \
  -H "Authorization: Bearer wsk_live_AbCd1234_xR9k..."

Scopes

Each key carries a list of scopes. Full access means the list is empty and the key inherits everything your plan permits. Otherwise the request is matched against the granted list and rejected with 403 missing_scope if a scope is missing. Scope names are stable across versions.

Least privilege
Issue one key per integration, with the smallest scope set that gets the job done. A CI bot that posts daily reports doesn't need payments:write.
ScopeGroupUsePlan
company:readReadOrg name, plan, KYB status, legal dataAny paid plan
freelancers:readReadList + view freelancers collaborating with the orgAny paid plan
projects:readReadList + view projects, status, budgetAny paid plan
payments:readReadInbound payments, statuses, amountsAny paid plan
invoices:readReadIssued + received invoicesAny paid plan
contracts:readReadMSAs, SOWs, signature statusAny paid plan
timesheets:readReadSubmitted + approved hoursAny paid plan
webhooks:readReadWebhook delivery history (when webhooks ship)Any paid plan
freelancers:writeWriteInvite, update, removeAny paid plan
projects:writeWriteCreate, update, archiveAny paid plan
payments:writeWriteCreate payment links, refundGlobal+
invoices:writeWriteGenerate consolidated, edit draftsGlobal+
contracts:writeWriteGenerate, send for signatureGlobal+
timesheets:writeWriteSubmit, approve, reject hoursAny paid plan

Live vs test

Keys are bound to one environment. Live keys read and write your real production data. Test keys hit the same endpoints but only see sandbox organizations and sandbox payments. Today the test sandbox is a private preview; ask your CSM if you need it provisioned.

No cross-talk
A live key cannot read sandbox data, and vice versa. The environment is part of the prefix on the wire, so we reject mismatches before any business logic runs.

Rotation

Rotation is a deliberate two-step flow. We don't support “regenerate this key in place” because in-place rotation always creates a window where two systems disagree about which secret is current.

  1. Create a new key with the same scopes. The old one keeps working.
  2. Switch your client config to the new key, deploy, and confirm the new key's lastUsedAt updates in the dashboard.
  3. Revoke the old key. If something goes wrong before the next deploy, an admin can restore it from the dashboard within seconds (the key row stays for audit).

Expiration

Set expiresAt at creation if the integration has a finite lifetime (a partner POC, a one-month migration, an audit). After the timestamp, requests fail with 401 expired and the row stays for audit. We send the workspace owner an email 14 days, 7 days, and 24 hours before expiry; you can't turn this off.

IP allowlist

Enterprise plans can lock each key to a list of CIDR blocks. Requests from outside the allowlist are rejected at the edge with 403 ip_not_allowed before the key is even resolved.

Available onEnterprise

Security model summary

  • Hashed at rest. bcrypt for legacy keys migrated from our previous platform; sha256 + constant-time compare for new keys. The validator picks the right algorithm from the row.
  • Logged. Every request lands in api_key_log: method, path, status, response time, IP, user agent. Queryable from your workspace and via the audit endpoints.
  • TLS only.The API answers on HTTPS only. HTTP requests are 308-redirected, but you should not rely on that — clients that send credentials over HTTP have already leaked them.
  • No CORS.The public REST surface does not set CORS headers. It's for server-to-server traffic; calling it from a browser would expose the key in the source.

Next