Standout API
List your open roles to power your own careers page, and add candidates to your Standout pipeline from your own systems: your careers page, your ATS, or your own AI agent.
Every candidate you add lands on the role's board in Pending review, exactly as if a teammate had added them by hand. Standout never contacts a candidate you add through the API.
Authentication
Create a key in Standout under Integrations → Standout API (company admins only). Send it as a bearer token:
Authorization: Bearer sk_standout_...
Keep the key on your server. Never put it in browser code: anyone who has it can add candidates to your roles. You can revoke a key at any time from the same place; it stops working immediately.
Base URL: https://standout.work/api/v1
List your roles
GET /api/v1/jobs
Your live roles, newest first, with everything you need to publish them: title, team, employment type, locations, work modes, the full description (as written and as sanitised HTML), and the salary and equity ranges. Salary, equity and location only appear when the role's disclosure settings allow it, so the response is safe to put on a public page.
| Query param | Default | Notes |
|---|---|---|
limit | 20 | Roles per page, 1 to 50. |
cursor | none | nextCursor from the previous page. Treat it as opaque. |
curl "https://standout.work/api/v1/jobs?limit=20" \
-H "Authorization: Bearer $STANDOUT_API_KEY"
{
"jobs": [
{
"id": "3f1c2a4e-8b7d-4c1a-9e2f-5a6b7c8d9e0f",
"title": "Founding Engineer",
"team": "Engineering",
"employmentType": "Full-time",
"locations": ["Paris", "Remote (France)"],
"workModes": ["HYBRID"],
"description": "## About the role\n\nYou'll build ...",
"descriptionHtml": "<h2>About the role</h2><p>You'll build ...</p>",
"salary": { "min": 80000, "max": 100000, "currency": "EUR" },
"equity": { "min": 0.25, "max": 0.5 },
"createdAt": "2026-09-24T08:14:03.920Z",
"updatedAt": "2026-09-26T11:02:40.118Z"
}
],
"nextCursor": "WyIyMDI2LTA5LTI0VDA4OjE0OjAzLjkyMFoiLCIzZjFjMmE0ZS..."
}
workModesis any ofREMOTE,HYBRID,IN_OFFICE,FLEXIBLE.descriptionis the text as written in Standout (usually Markdown);descriptionHtmlis the same text rendered and sanitised, ready to insert.salaryandequityarenullwhen unset or not disclosed. Equity is a percentage.nextCursorisnullon the last page. Pages are stable: a role published while you page never repeats or shifts one.
Fetch every page:
async function listAllRoles() {
const roles = [];
let cursor: string | null = null;
do {
const url = new URL("https://standout.work/api/v1/jobs");
url.searchParams.set("limit", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.STANDOUT_API_KEY}` },
});
if (!response.ok) throw new Error(`Standout ${response.status}`);
const page = await response.json();
roles.push(...page.jobs);
cursor = page.nextCursor;
} while (cursor);
return roles;
}
Cache the result on your side (a few minutes is plenty) rather than calling Standout on every page view of your careers site.
Add a candidate
POST /api/v1/candidates
Identify the person by their LinkedIn profile whenever you have it. We look them up and fill in their name, title and photo within about a minute. When you only have a name and email (a CV without a LinkedIn link), send firstName and email instead.
| Field | Type | Required | Notes |
|---|---|---|---|
jobId | uuid | yes | From GET /api/v1/jobs. |
linkedinUrl | string | one of these two | Profile URL or slug, e.g. https://www.linkedin.com/in/ada-lovelace. |
firstName | string | one of these two | Used when there is no LinkedIn profile. |
lastName | string | no | |
email | string | no | Stored on the candidate. |
phone | string | no | Stored on the candidate. |
githubUrl | string | no | GitHub profile URL or handle. |
note | string | no | Shown on the candidate's card to your team, e.g. "Applied via careers page". Max 4000 characters. |
requestId | uuid | no | Idempotency key. Retrying with the same requestId never adds the person twice. |
curl -X POST https://standout.work/api/v1/candidates \
-H "Authorization: Bearer $STANDOUT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobId": "3f1c2a4e-8b7d-4c1a-9e2f-5a6b7c8d9e0f",
"linkedinUrl": "https://www.linkedin.com/in/ada-lovelace",
"email": "[email protected]",
"note": "Applied via careers page",
"requestId": "9b2e6f1a-3c4d-4e5f-8a7b-1c2d3e4f5a6b"
}'
201 Created when the candidate was added, 200 OK when they were already on this role (nothing changes):
{
"candidateId": "5d9e1f2a-6b7c-4d8e-9f0a-1b2c3d4e5f6a",
"matchId": "9b2e6f1a-3c4d-4e5f-8a7b-1c2d3e4f5a6b",
"jobId": "3f1c2a4e-8b7d-4c1a-9e2f-5a6b7c8d9e0f",
"created": true,
"candidateName": "Ada Lovelace",
"linkedinId": "ada-lovelace",
"status": "PENDING",
"pipelineUrl": "https://standout.work/org?job=3f1c2a4e-8b7d-4c1a-9e2f-5a6b7c8d9e0f"
}
Errors
Every error has the same shape:
{ "error": "Human-readable message", "code": "JOB_UNAVAILABLE" }
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_INPUT | The body or query failed validation (including a bad cursor). details lists each problem. |
| 401 | UNAUTHORIZED | Missing, unknown or revoked key. |
| 404 | JOB_UNAVAILABLE | The role doesn't exist, isn't yours, or is archived. |
| 409 | MATCH_CONFLICT | The person already went through this role's pipeline, or has more than one record on it. |
| 409 | REQUEST_CONFLICT | This requestId was already used for a different candidate. |
| 422 | PROFILE_UNREADABLE | We couldn't read that LinkedIn profile. Nothing was added; check the link. |
| 429 | RATE_LIMITED | A limit below was reached. |
| 500 | UNEXPECTED | Something went wrong on our side. Retry with the same requestId. |
Limits
| Limit | Value | When exceeded |
|---|---|---|
| Roles per page | 50 | 400 INVALID_INPUT |
| Candidates added per key | 200 per day (UTC) | 429 RATE_LIMITED |
| Role listing requests per key | 1000 per hour | 429 RATE_LIMITED, with Retry-After |
| Requests per IP address | 3000 per hour | 429 RATE_LIMITED, with Retry-After |
Need more? Tell us.
Example: forward careers-page applications
Call the API from your server when someone applies, never from the browser:
export async function onApplication(application: {
jobId: string;
linkedinUrl?: string;
firstName: string;
lastName?: string;
email: string;
applicationId: string; // a uuid you already store
}) {
const response = await fetch("https://standout.work/api/v1/candidates", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STANDOUT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
jobId: application.jobId,
...(application.linkedinUrl
? { linkedinUrl: application.linkedinUrl }
: {
firstName: application.firstName,
lastName: application.lastName,
}),
email: application.email,
note: "Applied via careers page",
requestId: application.applicationId,
}),
});
if (!response.ok) {
const { code, error } = await response.json();
throw new Error(`Standout ${code}: ${error}`);
}
return response.json();
}