Skip to content
Developer

Scopable API: Getting Started Guide

Create an API key, send the first authenticated request, and handle paging, idempotent writes, errors, and signed webhooks.

Scopable Team8 min read

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

  1. Ask Scopable to enable API access for the workspace. The Developer tab appears after it is enabled.
  2. Sign in as a workspace admin.
  3. Go to Settings → Developer.
  4. Create a key and grant only the <domain>:read or <domain>:write scopes the integration needs. A read scope does not include write.
  5. 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:

MethodRouteWhat it does
GET/v1/clientsList clients
GET/v1/clients/{clientId}Get one client
POST/v1/clients/{clientId}Update a client
GET/v1/contacts/searchSearch contacts, optionally by clientId
POST/v1/contactsCreate a contact
POST/v1/contacts/{contactId}Update a contact
POST/v1/contacts/{contactId}/archiveArchive a contact
POST/v1/contacts/{contactId}/restoreRestore an archived contact
POST/v1/contacts/{contactId}/set-defaultMake 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.

  1. Send limit from 1 to 200. The default is 50.
  2. Read nextCursor. Pass it back as cursor on the next request, with the same filters.
  3. Stop when nextCursor is 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.

CodeHTTP statusMeaning
validation_failed400The body, query, cursor, or a required header is invalid
unauthorized401The key is missing, unknown, expired, or revoked
forbidden403The key is valid and lacks the scope, or the workspace refuses the call. See details.reason
not_found404No such record in this workspace
conflict409An Idempotency-Key was reused, or the record changed. See details.reason
approval_required202Agent credentials only. The command was recorded for a person to approve and did not run
rate_limited429Wait for Retry-After
unavailable503Retry 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.

EventWhen it is sent
client.updatedA client record changes
contact.createdA contact is created
contact.updatedA contact changes
contact.archivedA contact is archived
contact.restoredAn 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.