Your code authenticates to the Sureshake API with an API key. A key acts as the person who created it, so it can see and change only what that person can.
Create an API key
You create and manage keys in the product. The /v2/api-keys routes accept only a signed-in session, so a key cannot create or rotate another key.
- Sign in and open Settings.
- Open the Security section.
- Under API Keys, select Create API Key.
- Fill in the form:
- Key Name, which is required.
- Description (Optional).
- Permissions. See the note below.
- Expiration: Never, 30 days, 90 days or 1 year.
- Select Create Key and copy the key. Sureshake shows it once and keeps only a hash, so you cannot see it again.
The form also has a Rate Limit (requests/hour) field. It has no effect at the moment. Every key gets the same limits.
To replace a key, create the new one, switch your code to it, then select Revoke key on the old one in the list. A revoked key stops working at once.
Send the key
Send the key in the X-API-Key header:
curl -sS https://api.sureshake.com/v2/organizations/me \
-H "X-API-Key: $SURESHAKE_API_KEY"Authorization: Bearer sk_... works the same way on any route that accepts keys. If you send both, X-API-Key wins.
Use keys from a server. The API’s CORS rules do not allow the X-API-Key header, and a key in browser code is visible to anyone who loads the page.
Permissions
| Permission | Allows |
|---|---|
| Read | Routes that read data |
| Write | Routes that read or change data |
Which routes accept an API key
These routes accept a key:
| Area | Routes | Permission |
|---|---|---|
| Organizations | Everything under /v2/organizations: your organizations and invitations, members, creating an organization, the organization wallet |
Read for GET, Write otherwise |
| Directory search and profiles | GET /v2/directory, /summary, /compare, /{subjectType}/{slug} and its /record-summary and /profile-experience, and POST /v2/directory/engagement/clicks |
Optional. These work without a key |
| Directory listings | /v2/directory/listings/... (provision, draft, preview, publish, unpublish, rename, analytics, packages, claim requests, verification exceptions), /v2/directory/packages/{id}, /v2/directory/person-profile/publication, /v2/directory/entity-profile/{entityId}/publication, /v2/directory/workflows/{id}, /v2/directory/collections/{collectionId} |
Read for GET, Write otherwise |
| Directory follows and corrections | /v2/directory/{subjectType}/{listingId}/follow, /follow-state, /follow-preferences, /follow-requests, /corrections |
Read for GET, Write otherwise |
| Collections | Everything under /v2/entities/{entityId}/collections, plus GET /v2/collections/templates, GET /v2/collections/{id}/shared and POST /v2/collections/invites/{code} |
Read for GET and for POST .../agent/messages, Write otherwise |
| Companies | POST /v2/entities, and /v2/entities/{id}/models/{templateKey}/scenarios |
Read for GET, Write otherwise |
| Reports | GET /v2/reports/{id}, /document, /publication-composition, POST /v2/reports/{id}/views, POST /v2/reports/shares/{shareId}/viewed |
Read |
| Search | GET /v2/search/omni |
Optional. Without a key you get directory results only |
The other parts of the reference don’t accept keys at all: users, api-keys, filings, execution, analytics, billing, documents, hiring, vault, notifications, reporter, startups and watchlists.
Within the areas above, routes not listed here need a session too. For example, a key can read a report by ID but cannot list your reports, and it can create a company but cannot list your companies. To get an ID, open the company or report in Sureshake and copy it from the URL: /entities/<id> or /reports/<id>.
Errors when authenticating
| Status | error.code |
error.message |
Meaning |
|---|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or expired API key |
The key is wrong, revoked or expired |
| 401 | UNAUTHORIZED |
Authentication failed. |
The route does not accept keys, or you sent no credential |
| 403 | FORBIDDEN |
API key scope 'read' is not permitted for this operation (requires 'write') |
Your key’s permission is too low |
| 403 | FORBIDDEN |
Varies | The key is valid, but its owner has no access to this resource |
On routes where a credential is optional, such as GET /v2/directory and GET /v2/search/omni, an invalid key does not cause an error. The request runs as anonymous and you get public results only.
Response formats
Dates are ISO 8601 strings. Most routes return one of two shapes.
{
"data": { "id": "org-acme-holdings", "name": "Acme Holdings" },
"meta": { "timestamp": "2026-11-03T18:00:00.000Z" }
}{
"data": [{ "id": "org-acme-holdings" }],
"meta": { "total": 1, "limit": 20, "offset": 0, "hasMore": false }
}Some routes use their own shape. GET /v2/directory returns data, pagination, resultCount and facets. GET /v2/search/omni returns status, workspace and directory. The response schema on each route’s reference page is the one to follow.
Errors
Errors have this shape:
{
"error": {
"code": "NOT_FOUND",
"message": "Entity not found",
"details": {},
"traceId": "V1StGXR8_Z5jdHi6B-myT"
}
}Branch on the status and code, not on message. details and traceId are optional. Errors from the authentication step have no traceId in the body.
| Status | error.code |
Notes |
|---|---|---|
| 400 | FST_ERR_VALIDATION |
The request failed schema validation. message is a JSON array of issues as a string, each with a path and message |
| 400 | BAD_REQUEST |
The request was well formed but can’t be carried out |
| 401 | UNAUTHORIZED |
See errors when authenticating |
| 403 | FORBIDDEN |
You don’t have access |
| 404 | NOT_FOUND |
The resource wasn’t found |
| 409 | CONFLICT |
For example, an organization with this name or slug already exists |
| 410 | GONE |
The resource is no longer available |
| 412 | PRECONDITION_FAILED |
A condition for the operation isn’t met |
| 422 | UNPROCESSABLE_ENTITY |
See idempotency |
| 429 | TOO_MANY_REQUESTS, RATE_LIMITED |
See rate limits |
| 500 | INTERNAL_SERVER_ERROR |
An unexpected error. If it keeps happening, report it with the trace ID |
| 503 | DEPENDENCY_UNAVAILABLE |
Something the API depends on is down. Retry with backoff |
You may see other codes. Go by the status when you don’t recognize one.
Validation runs before authentication, so a malformed request can return 400 even if your key is invalid.
A path that doesn’t exist returns a different shape: { "message": "...", "error": "Not Found", "statusCode": 404 }.
Trace IDs
Every response has an x-trace-id header. You can also set your own by sending x-trace-id. Include the value when you report a problem.
Pagination
List routes page in one of two ways. The route’s reference page shows which.
Offset. Pass limit and offset. limit is usually 1 to 100 and defaults to 20. Keep adding limit to offset while meta.hasMore is true.
curl -sS "https://api.sureshake.com/v2/entities/$ENTITY_ID/collections?limit=100&offset=100" \
-H "X-API-Key: $SURESHAKE_API_KEY"Cursor. GET /v2/directory returns pagination.nextCursor. Pass it back as cursor while pagination.hasMore is true.
curl -sS "https://api.sureshake.com/v2/directory?limit=100&cursor=$NEXT_CURSOR" \
-H "X-API-Key: $SURESHAKE_API_KEY"Rate limits
| Applies to | Limit |
|---|---|
| All routes | 2,000 requests per minute for each key. Without a credential, the limit applies per IP address |
GET /v2/directory, /compare, /summary, GET /v2/search/omni |
240 per minute with a valid credential, 60 per minute without |
POST /v2/directory/engagement/clicks |
120 per minute with a valid credential, 30 per minute without |
/v2/mcp |
120 per minute |
Responses carry x-ratelimit-limit and x-ratelimit-remaining, plus x-ratelimit-reset. Don’t parse the reset value on its own: on the directory and search routes above it is a timestamp, and elsewhere it is a number of seconds.
When you go over a limit, you get a 429 with a retry-after header in seconds. Wait that long, then retry. The code is RATE_LIMITED on the directory and search routes above and TOO_MANY_REQUESTS elsewhere, so check the status rather than the code.
Idempotency
Some write routes let you retry safely. Send a fresh UUID in X-Idempotency-Key for each logical operation, and reuse it if you retry that operation.
With an API key, this works on these routes:
POST /v2/organizationsPOST /v2/organizations/{id}/membersPATCH /v2/organizations/members/{memberId}POST /v2/organizations/invitations/{memberId}/acceptPOST /v2/organizations/invitations/{memberId}/decline
Other routes ignore the header.
curl -sS -X POST https://api.sureshake.com/v2/organizations \
-H "X-API-Key: $SURESHAKE_API_KEY" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Holdings"}'This needs a key with Write permission, and it creates a real organization. On success you get 201 and the new organization in data.
| What happens | Response |
|---|---|
| Same key and body, and the first request finished with a 2xx | The stored response again, with x-idempotency-replayed: true |
| Same key, and the first request is still running | 409 CONFLICT |
| Same key, different method, URL or body | 422 UNPROCESSABLE_ENTITY |
| The first request failed | The key is released, so a retry runs the operation again |
Keys belong to the user who sent them, and only 2xx responses are stored.