Scopable API: Getting Started Guide
Create an API key, send the first authenticated request, and handle paging, idempotent writes, errors, and signed webhooks.
Prerequisites
- A Scopable workspace where API access has been enabled
- A workspace admin who can open Settings → Developer
Scopable has a full REST API (OpenAPI 3.1 at https://api.scopable.io/v1/openapi.json) covering 36 areas of the product, including clients, contacts, sites, opportunities, quotes, products, tickets, projects, agreements, compliance, assessments, QBRs, Microsoft 365 and Azure. Workspace admins create API keys under Settings → Developer with <domain>:read and <domain>:write scopes. API access is enabled per workspace on request. Scopable also runs a remote MCP server with OAuth.
This guide covers the first request, the client and contact examples, and the rules that apply to every call. The OpenAPI document is the field reference. The API page lists the 36 areas. The developer index is the short version for agents. Assistants that should sign in as a person use the MCP setup guide instead of an API key.
Tickets, projects, products, org, team members, and permission groups are read-only for API keys. Admin operations are refused to keys. Not every operation is writable.
What this guide covers
- Create a key under Settings → Developer after API access is enabled for the workspace.
- Send the first request to
GET /v1/clients?limit=1. - Read and write clients and contacts with the routes in the table below. Other areas use the same auth, paging, and idempotency rules.
- Receive outbound webhooks for client and contact changes.
Step 1: Generate an API key
- Ask Scopable to enable API access for the workspace. The Developer tab appears after it is enabled.
- Sign in as a workspace admin.
- Go to Settings → Developer.
- Create a key and grant only the
<domain>:reador<domain>:writescopes the integration needs. A read scope does not include write. - Copy the key immediately. It is shown once. If you lose it, create a replacement and revoke the old key.
Keys use the sk_live_ or sk_test_ prefix. Store the key in an environment variable or a secrets manager.
Step 2: Authenticate your first request
Every request sends the key as a bearer token. The base URL is https://api.scopable.io. Routes sit under /v1.
curl -sS "https://api.scopable.io/v1/clients?limit=1" \
-H "Authorization: Bearer $SCOPABLE_API_KEY"
A request with no key, or with a key Scopable does not recognize, returns HTTP 401 and the error code unauthorized. That is the expected result before a key is attached.
An authorized list response has this shape. data holds the records for that page. Field names for a client are in the OpenAPI document.
{ "data": [], "nextCursor": null }
Step 3: Read and write records
Search contacts by name or email, optionally within one client:
curl -sS "https://api.scopable.io/v1/contacts/search?q=ada&clientId=<client-id>&limit=25" \
-H "Authorization: Bearer $SCOPABLE_API_KEY"
Create a contact under a client. The body below uses placeholder values:
curl -sS https://api.scopable.io/v1/contacts \
-H "Authorization: Bearer $SCOPABLE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: contact-example-001" \
-d '{"clientId":"<client-id>","name":"Example Contact","email":"contact@example.com"}'
Update a client. Fields you leave out stay as they are. A field sent as null is cleared:
curl -sS -X POST "https://api.scopable.io/v1/clients/<client-id>" \
-H "Authorization: Bearer $SCOPABLE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: client-website-001" \
-d '{"website":"https://example.com"}'
Archive a contact:
curl -sS -X POST "https://api.scopable.io/v1/contacts/<contact-id>/archive" \
-H "Authorization: Bearer $SCOPABLE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: contact-archive-001" \
-d '{}'
The client and contact routes used above:
| Method | Route | What it does |
|---|---|---|
GET | /v1/clients | List clients |
GET | /v1/clients/{clientId} | Get one client |
POST | /v1/clients/{clientId} | Update a client |
GET | /v1/contacts/search | Search contacts, optionally by clientId |
POST | /v1/contacts | Create a contact |
POST | /v1/contacts/{contactId} | Update a contact |
POST | /v1/contacts/{contactId}/archive | Archive a contact |
POST | /v1/contacts/{contactId}/restore | Restore an archived contact |
POST | /v1/contacts/{contactId}/set-default | Make a contact the client's default |
The OpenAPI document lists the other routes, request fields, and response fields. You can read it without an API key.
Pagination
Lists and searches page with a cursor.
- Send
limitfrom 1 to 200. The default is 50. - Read
nextCursor. Pass it back ascursoron the next request, with the same filters. - Stop when
nextCursoris null.
Treat the cursor as opaque. A cursor sent with different filters, or one Scopable did not issue, returns validation_failed.
Idempotent writes
Every write (POST, PATCH, PUT, or DELETE) requires an Idempotency-Key header. The key is opaque and at most 255 characters. A write without one returns validation_failed.
Send the same key again when you retry a request you are not sure landed. Reuse returns conflict, and details.reason names the conflict. Use a new key for a new change.
Error handling
Errors use one envelope. Some responses add details, including details.reason. The same requestId is in the X-Request-Id header.
{
"error": {
"code": "unauthorized",
"type": "https://api.scopable.io/v1/openapi.json#/components/schemas/UnauthorizedError",
"message": "string",
"requestId": "string"
}
}
type is a URI. For unauthorized it is the schema URL in the example. Each code has its own type in the OpenAPI document. message and requestId are strings from the response, not the placeholders above.
| Code | HTTP status | Meaning |
|---|---|---|
validation_failed | 400 | The body, query, cursor, or a required header is invalid |
unauthorized | 401 | The key is missing, unknown, expired, or revoked |
forbidden | 403 | The key is valid and lacks the scope, or the workspace refuses the call. See details.reason |
not_found | 404 | No such record in this workspace |
conflict | 409 | An Idempotency-Key was reused, or the record changed. See details.reason |
approval_required | 202 | Agent credentials only. The command was recorded for a person to approve and did not run |
rate_limited | 429 | Wait for Retry-After |
unavailable | 503 | Retry later. Quote requestId if you contact support |
Responses report RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and RateLimit-Policy. A 429 also returns Retry-After.
Breaking changes require a new major URL version. A scheduled retirement sends Deprecation, Sunset, and a Link with rel="deprecation". No v1 retirement is currently scheduled.
Outbound webhooks
Scopable can POST client and contact changes to an HTTPS endpoint you register under Settings → Developer → Webhooks. These events are not operations in the OpenAPI document.
| Event | When it is sent |
|---|---|
client.updated | A client record changes |
contact.created | A contact is created |
contact.updated | A contact changes |
contact.archived | A contact is archived |
contact.restored | An archived contact is restored |
The JSON body has event, data, and timestamp. Return a 2xx status within 10 seconds. Failed deliveries retry with exponential backoff, so your handler should tolerate a duplicate delivery.
Verify webhook signatures
X-Scopable-Signature is the hex HMAC-SHA256 of the raw request body, signed with that endpoint's secret. Compare the header to a digest you compute yourself, in constant time, before you trust the body.
import { createHmac, timingSafeEqual } from "node:crypto";
export function signaturesMatch(secret, rawBody, header) {
const digest = createHmac("sha256", secret).update(rawBody).digest("hex");
const left = Buffer.from(digest);
const right = Buffer.from(header ?? "", "utf8");
return left.length === right.length && timingSafeEqual(left, right);
}
Keep the endpoint secret with the same care as an API key. Reject the request when the header is missing or the comparison fails.
Security and operations notes
- Grant only the scopes the integration uses.
- Confirm the target workspace before a write.
- Rotate a key that was exposed, and revoke unused keys from Settings → Developer.
- Scopable does not show the full key again after creation.
Frequently asked questions
Do I need OAuth to use the API?
No. The calls in this guide use a bearer API key. The MCP server uses OAuth so a person can connect an assistant.
Where are the other operations?
The OpenAPI document lists all 36 areas. Your key needs the scope for each operation, and some areas are read-only for keys.
What if two systems create the same contact?
Each create with a new Idempotency-Key creates a new contact. Search first with GET /v1/contacts/search?q=<email> when the contact might already exist.
Does archiving permanently remove a contact?
No. Archiving hides the contact and clears its default-contact status. POST /v1/contacts/{contactId}/restore brings it back. Restoring does not make it the default contact again.
Where do I manage keys?
Settings → Developer: create, scope, and revoke API keys, and configure outbound webhooks.
Related articles
Still stuck? Email … with a screenshot of the error.