> ## Documentation Index
> Fetch the complete documentation index at: https://vytral-dependabot-npm-and-yarn-development-95cc887cce.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# API overview

> Build integrations against Harly's versioned REST API with scoped keys, pagination, idempotency, and safe retries.

The Harly REST API is available at:

```text theme={null}
https://hiring.example.com/api/v1
```

The OpenAPI document is available from a running installation at
`/api/v1/openapi.json`. Use it as the source of truth for exact schemas and
route parameters.

The repository also includes a generated snapshot at `/openapi.json`. With the
Harly dev server running on port 3000, refresh it after changing an API
contract with:

```bash theme={null}
pnpm generate:openapi
```

## Authenticate

Create a key from **Settings → Developers → API keys**, copy the raw value
once, and send it as a bearer token:

```bash theme={null}
curl --fail-with-body \
  -H 'Authorization: Bearer harly_sk_live_YOUR_KEY' \
  -H 'Accept: application/json' \
  'https://hiring.example.com/api/v1/jobs?limit=25'
```

Keys are scoped. Give an integration only the resources it needs. Rotate or
revoke keys when an owner or vendor changes.

## Response envelopes

### Success

```json theme={null}
{
  "data": { ... },
  "meta": {
    "nextCursor": "cursor_01j1abc"
  }
}
```

### Error

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "A job title is required",
    "details": { "field": "title" }
  }
}
```

Common error codes: `validation_error`, `not_found`, `forbidden`,
`conflict`, `rate_limited`.

## Pagination

List endpoints use cursor-based pagination:

```bash theme={null}
# First page
curl 'https://hiring.example.com/api/v1/candidates?limit=50'

# Next page — use the cursor from meta.nextCursor
curl 'https://hiring.example.com/api/v1/candidates?limit=50&cursor=cursor_01j1abc'
```

* Do not assume a stable sort order or page number.
* `meta.nextCursor` is absent when you have reached the last page.
* `limit` defaults to `25` and caps at `100`.

## Idempotency

POST requests that support it accept an `Idempotency-Key` header. Use a
unique value per logical operation — for example a UUID or a stable key
derived from the operation's inputs:

```bash theme={null}
curl -X POST \
  'https://hiring.example.com/api/v1/applications/app_123/move' \
  -H 'Authorization: Bearer harly_sk_live_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: move-app_123-2026-07-27T12:00:00Z' \
  --data '{"toStageId":"stage_interview"}'
```

<Warning>
  A `409` may mean a real concurrency conflict or an idempotency collision.
  Inspect the error envelope before retrying a mutation. Do not retry a `409`
  blindly — the operation may have already succeeded.
</Warning>

## Rate limits

The API applies per-key rate limits. When a response returns `429`, back off
using the `Retry-After` header value before retrying. Do not hammer the API
with immediate retries.

```bash theme={null}
# Check the Retry-After header
curl -I 'https://hiring.example.com/api/v1/jobs'
# Retry-After: 30
```

## HTTP status codes

| Status | Meaning                           | Client action                           |
| -----: | --------------------------------- | --------------------------------------- |
|  `200` | Success                           | Process the response                    |
|  `201` | Created                           | Record the new resource ID              |
|  `204` | No content (delete)               | Success, no body                        |
|  `400` | Malformed request                 | Fix the request body or params          |
|  `401` | Missing or invalid key            | Rotate or replace the key               |
|  `403` | Missing scope or workspace access | Request the narrowest required scope    |
|  `404` | Resource not found                | Reconcile IDs and workspace             |
|  `409` | Conflict or idempotency collision | Inspect the error before retrying       |
|  `422` | Valid JSON with invalid fields    | Show validation details to the operator |
|  `429` | Rate limited                      | Back off and respect `Retry-After`      |
|  `5xx` | Server error                      | Retry with exponential backoff          |

## Main resources

Jobs, candidates, applications, interviews, offers, scorecards, tasks,
pool entries, activity events, webhooks, and API keys are available through
the versioned API.

Use [webhooks](/developers/webhooks) to react to changes instead of polling
every resource. See [API resource reference](/developers/api-reference) for
the full scope and operation inventory.

## Try it

The OpenAPI document at `/api/v1/openapi.json` is importable into Insomnia,
Postman, or any OpenAPI-compatible tool:

```bash theme={null}
curl -o harly-openapi.json \
  'https://hiring.example.com/api/v1/openapi.json'
```

See the [API integration tutorial](/tutorials/api-integration) for an
end-to-end walkthrough with webhook verification.

## Public jobs and applications API

The public API is the same API used by Harly's embedded job widget. It is
CORS-enabled and intentionally exposes only published, open jobs and the
application intake flow. It never exposes candidate or application records
after submission.

Every request must identify the workspace in one of these ways:

* Add `?workspace=<workspace-slug>` to the URL. This is zero-configuration and
  is suitable when the public workspace slug is already known.
* Send a publishable key (`pk_`) in `X-API-Key`, `Authorization: Bearer`, or
  `?pk=`. The key is bound to one workspace and is useful for browser embeds,
  analytics, and revocation.

The public routes do not infer a workspace automatically, even on a
single-workspace installation. Never send a secret (`sk_`) key to a browser.

### 1. List open jobs

```bash theme={null}
curl 'https://hiring.example.com/api/public/v1/jobs?workspace=acme'
```

With a publishable key:

```bash theme={null}
curl \
  -H 'X-API-Key: harly_pk_live_YOUR_PUBLISHABLE_KEY' \
  'https://hiring.example.com/api/public/v1/jobs'
```

The list endpoint supports the optional `department`, `location`,
`workplaceType`, and `q` query filters. It returns a standard `{ data }`
success envelope containing the workspace branding and serialized public jobs.

Use the returned job's `slug` as the public job identifier. A custom frontend
should use the slug rather than the internal job `id`.

### 2. Fetch a job and its application configuration

```bash theme={null}
curl \
  'https://hiring.example.com/api/public/v1/jobs/senior-frontend-engineer?workspace=acme'
```

The response includes `applicationConfig`. Its `questions` array is the source
of truth for custom questions:

```json theme={null}
{
  "id": "q_why-us",
  "label": "Why do you want to join us?",
  "type": "textarea",
  "required": true,
  "minLength": 50,
  "placeholder": "Tell us why…"
}
```

The question's `id` is the key used in `questionAnswers`. Supported question
types are `text`, `textarea`, `url`, and `select`; `select` questions also
return an `options` array. The same response describes standard fields under
`applicationConfig.sections`, including resume, links, education, experience,
and cover letter visibility (`required`, `optional`, or `disabled`).

### 3. Submit the application

```js theme={null}
const response = await fetch(
  "https://hiring.example.com/api/public/v1/jobs/senior-frontend-engineer/applications?workspace=acme",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      firstName: "Ada",
      lastName: "Lovelace",
      email: "ada@example.com",
      phone: "+44 20 7946 0958",
      linkedinUrl: "https://linkedin.com/in/ada-lovelace",
      questionAnswers: {
        "q_why-us": "Because…",
      },
      // Include this only when the workspace has CAPTCHA enabled.
      captchaToken: "token-from-your-captcha-provider",
      _hp: "",
    }),
  },
);

if (!response.ok) {
  const error = await response.json();
  // 422 includes field-level validation details.
  throw new Error(error.error?.message ?? "Application failed");
}
```

The endpoint is:

```text theme={null}
POST /api/public/v1/jobs/{jobSlug}/applications
```

It returns `201` with this success envelope:

```json theme={null}
{
  "data": {
    "received": true,
    "message": "Application received."
  }
}
```

The API validates required standard fields and every custom answer, so the
frontend should render from `applicationConfig` instead of duplicating the
form configuration. If CAPTCHA is configured for the workspace, the
application request must include a valid token in `captchaToken` (the legacy
`turnstileToken` name is also accepted).

The public application endpoint is rate-limited to protect the intake flow.
Clients must handle `429` and retry after the `Retry-After` response header.
The request also accepts the `_hp` honeypot field; real forms should leave it
empty.

### Uploads

For a resume or profile image, request a presigned upload URL first, upload the
file directly to that URL, and send the returned `key` and file metadata in the
application payload:

```text theme={null}
POST /api/public/v1/resume/presign
POST /api/public/v1/image/presign
```

Both endpoints accept `{ filename, contentType, contentLength }` and are scoped
to the same `workspace` or publishable key. The resume fields are
`resumeKey`, `resumeFileName`, `resumeFileType`, and `resumeFileSize`; the image
field is `photoUrl`.

For a browser integration, the publishable-key variant is usually preferable:

```js theme={null}
const headers = {
  "Content-Type": "application/json",
  "X-API-Key": "harly_pk_live_YOUR_PUBLISHABLE_KEY",
};
```

Never expose a secret (`sk_`) key in frontend code.

### Public API errors

Public routes use the same error envelope as the authenticated API:

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "Validation failed.",
    "details": {}
  }
}
```

Common statuses are `400` for a missing or malformed workspace selector, `403`
for a failed CAPTCHA or missing publishable-key scope, `404` for a job that is
not public, `422` for invalid application fields, and `429` for rate limiting.
