# Career Site Auto Apply (`jobo.world/career-site-auto-apply`) Actor

Apply to public Ashby jobs through a secure start-and-answer API. Receive employer questions, submit complete answers, and pay once per accepted attempt.

- **URL**: https://apify.com/jobo.world/career-site-auto-apply.md
- **Developed by:** [Jobo](https://apify.com/jobo.world) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 application createds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Career Site Auto Apply

Apply to public Ashby jobs through a two-endpoint API:

`start → questions → answers → next questions or final result`

The Actor runs continuously in **Apify Standby mode**. Jobo owns the browser session and outbound proxy; the Actor authenticates the Apify caller, protects application sessions, and charges once for an accepted application attempt. You can start through the Actor input form or the HTTP API. Answer rounds always continue through the Standby API; no dataset is written.

> **This API submits real job applications.** Once all required answer steps are accepted, Jobo submits the form to the employer automatically. Test only with vacancies that are explicitly identified as sandboxes or test postings.

### Supported jobs: Ashby only

This release supports public jobs hosted on `jobs.ashbyhq.com`. Use either of these URL shapes:

```text
https://jobs.ashbyhq.com/{organization-slug}/{job-uuid}
https://jobs.ashbyhq.com/{organization-slug}/{job-uuid}/application
```

Example:

```text
https://jobs.ashbyhq.com/brighthire-sandbox/271829e3-b51f-4304-8e04-c7627d807707/application
```

The organization slug may contain letters, numbers, hyphens, underscores, and
interior dots. It must start alphanumerically and cannot end with a dot or
contain consecutive dots. The job ID must be a UUID. A trailing slash, query
string, or fragment is allowed.

The following are not supported:

- Ashby board homepages without a job UUID;
- application-request, survey, assignment, event, or authenticated Ashby URLs;
- embedded or custom-domain Ashby career sites; and
- Greenhouse, Lever, Workday, or any other non-Ashby ATS.

You may send a Jobo `job_id` instead of an `apply_url`, but that job must resolve to a supported Ashby URL. Otherwise the Actor cancels the upstream attempt before billing.

A valid HTTPS URL outside this scope returns the following response and is **not charged**:

```http
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
```

```json
{
  "type": "https://apify.com/jobo.world/career-site-auto-apply/errors/unsupported_ats",
  "title": "Unsupported ATS",
  "status": 422,
  "detail": "This Actor currently supports only public Ashby job URLs in the documented jobs.ashbyhq.com format.",
  "code": "unsupported_ats",
  "api_version": "v1"
}
```

### Price

`application-created` costs **$0.10 once per accepted application attempt**. Platform usage is included; there is no compute, proxy, or per-step charge.

- Malformed requests, unsupported targets, and starts rejected before Jobo creates an application are free.
- Replays and concurrent starts produce at most one charge.
- An accepted attempt remains billable if the employer's ATS later rejects or fails it.
- If the Apify spending limit prevents charging, the Actor cancels the new application and returns `402 spending_limit_reached`.

### Quick start

Call the authenticated Standby URL using your Apify API token. Keep the token, idempotency key, and returned Jobo session token private.

#### Optional: start from the Actor input form

In Apify Console, enter the Ashby `apply_url` and run the Actor. The run makes the same initial application call and writes the response to the default key-value store as `OUTPUT`. It includes the first questions (or terminal result), a `session_token`, `next_action`, and the effective `idempotency_key`.

The input form handles only the initial call. Use `POST /v1/applications/answers` below for every answer round. If the run returns `next_action: retry_start`, repeat `POST /v1/applications` with the returned `idempotency_key` and the same URL. Supplying your own optional `idempotency_key` makes re-attaching across Actor runs straightforward.

#### 1. Start or re-attach

```bash
curl --request POST \
  "https://jobo-world--career-site-auto-apply.apify.actor/v1/applications" \
  --header "Authorization: Bearer $APIFY_TOKEN" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: candidate-42-brighthire-test" \
  --data '{
    "apply_url":"https://jobs.ashbyhq.com/brighthire-sandbox/271829e3-b51f-4304-8e04-c7627d807707/application"
  }'
```

Send exactly one target:

- `apply_url`: a supported public Ashby job URL; or
- `job_id`: a Jobo job UUID whose application URL is supported by this Actor.

A successful answerable response resembles this abridged example:

```json
{
  "session_token": "jobo1.…",
  "session_expires_at": "2026-08-31T04:00:00.000Z",
  "next_action": "submit_answers",
  "application": {
    "api_version": "2026-08-31",
    "id": "11111111-1111-4111-8111-111111111111",
    "provider_id": "ashby",
    "status": "awaiting_answers",
    "current_step": {
      "id": "22222222-2222-4222-8222-222222222222",
      "correction_round": 0,
      "answers_expire_at": "2026-08-30T04:10:00.000Z",
      "fields": [
        {
          "field_id": "_systemfield_name",
          "label": "Name",
          "type": "text",
          "requires_answer": true
        },
        {
          "field_id": "_systemfield_resume",
          "label": "Resume",
          "type": "file",
          "requires_answer": true
        }
      ]
    }
  }
}
```

If the response is `202` with `next_action: retry_start`, repeat the **same body and Idempotency-Key**. Two Apify users cannot collide because the Actor namespaces the upstream key to the authenticated user. Reusing one key for a different target returns `409 idempotency_key_reuse`.

#### 2. Submit a complete answer snapshot

Answer every field in `application.current_step` whose `requires_answer` is `true`. Use the latest session token and echo the step's `correction_round`.

```bash
curl --request POST \
  "https://jobo-world--career-site-auto-apply.apify.actor/v1/applications/answers" \
  --header "Authorization: Bearer $APIFY_TOKEN" \
  --header "Content-Type: application/json" \
  --header "X-Jobo-Session: $JOBO_SESSION" \
  --data '{
    "correction_round": 0,
    "answers": [
      {"field_id":"_systemfield_name","value":"Ada Lovelace"},
      {
        "field_id":"_systemfield_resume",
        "value":{
          "url":"https://files.example.com/temporary/resume.pdf?signature=…",
          "filename":"resume.pdf",
          "content_type":"application/pdf"
        }
      }
    ]
  }'
```

Before writing, the Actor refreshes the current Jobo state. If the token's step or correction round was already processed, it returns the refreshed application and a rotated token without applying the snapshot twice.

Follow `next_action` until it says `complete`:

| Value | What to do |
|---|---|
| `retry_start` | Repeat the exact start body and `Idempotency-Key`. |
| `submit_answers` | Answer the latest `application.current_step` using the latest session token. |
| `retry_answers` | Repeat the exact answers body and the same session token. |
| `complete` | Stop. Inspect `application.status` and `application.failure`. |

Terminal statuses are `submitted`, `failed`, and `canceled`. A `202` answer response, connection loss, or client timeout is not proof of failure: replay the same token and complete body to re-attach safely.

### JavaScript loop

This complete control-flow helper leaves only field answering to your application:

```js
const ACTOR_URL = "https://jobo-world--career-site-auto-apply.apify.actor";
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));

export async function applyToAshby({ apifyToken, applyUrl, idempotencyKey, answerStep }) {
  const call = async (path, headers, body) => {
    const response = await fetch(`${ACTOR_URL}${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apifyToken}`,
        "Content-Type": "application/json",
        ...headers,
      },
      body: JSON.stringify(body),
    });
    const payload = await response.json();
    if (response.status !== 200 && response.status !== 202) {
      const error = new Error(payload.detail ?? `Request failed with ${response.status}`);
      error.status = response.status;
      error.problem = payload;
      throw error;
    }
    const retryAfterSeconds = Number(response.headers.get("Retry-After") ?? "1");
    return {
      payload,
      retryAfterMs: Number.isFinite(retryAfterSeconds)
        ? Math.max(1, retryAfterSeconds) * 1000
        : 1000,
    };
  };

  let result;
  let reply;
  do {
    reply = await call(
      "/v1/applications",
      { "Idempotency-Key": idempotencyKey },
      { apply_url: applyUrl },
    );
    result = reply.payload;
    if (result.next_action === "retry_start") await sleep(reply.retryAfterMs);
  } while (result.next_action === "retry_start");

  while (result.next_action === "submit_answers") {
    const step = result.application.current_step;
    const body = {
      correction_round: step.correction_round,
      answers: await answerStep(step.fields.filter((field) => field.requires_answer)),
    };
    const session = result.session_token;
    do {
      reply = await call(
        "/v1/applications/answers",
        { "X-Jobo-Session": session },
        body,
      );
      result = reply.payload;
      if (result.next_action === "retry_answers") await sleep(reply.retryAfterMs);
    } while (result.next_action === "retry_answers");
  }

  return result.application;
}
```

`answerStep(fields)` must return a complete `[{ field_id, value }]` array for the supplied fields. Production clients should also add bounded retry handling for `429`, `502`, `503`, and `504`; respect `Retry-After` when present.

### Answer value shapes

Use the value shape matching each field's `type`:

| Field type | Value |
|---|---|
| `text`, `textarea`, `select`, `radio` | string; select/radio must use an advertised option `value` |
| `multi_select` | array of advertised option values |
| `checkbox` | boolean |
| `number` | number |
| `date` | `YYYY-MM-DD` string |
| `partial_date` | `YYYY`, `YYYY-MM`, or `YYYY-MM-DD` |
| `typeahead` | `{"query":"…","selection":{"value":"…","label":"…"}}` |
| `file` | `{"url":"https://…","filename":"resume.pdf","content_type":"application/pdf"}` |
| `repeating_group` | array of objects keyed by the advertised `item_fields[].key` |

For file answers, `url` must be a public or temporary signed **HTTPS URL that remains downloadable by Jobo** while the step is processed. The Actor has no upload endpoint. Local paths, data URLs, and URLs requiring custom request headers do not work.

Submit fields whose `requires_answer` is `true` even when the correct value is false, zero, or an allowed empty collection. Synchronous field problems return `400 validation_failed`; they are free and do not consume a correction round.

### Correction rounds and deadlines

Ashby can reject a value after browser submission. Jobo then returns the step with a higher `correction_round` and `command_errors`. Build a corrected, complete snapshot from the new `current_step`, use the new token, and echo the new round. A stale round returns `409 stale_correction_round`.

Every answerable step includes `answers_expire_at`. Missing it produces a terminal failure such as `answers_timeout` or `verification_timeout`. Session access expires 24 hours after application creation and is not extended by later calls.

### Errors and retries

| HTTP | Typical code | Meaning |
|---:|---|---|
| `400` | `invalid_request`, `validation_failed` | Fix the request shape or field answers before retrying. |
| `401` | `invalid_session` | The Jobo session is expired, altered, or belongs to another Apify user. |
| `402` | `spending_limit_reached` | Increase the Apify run spending limit, then retry the start safely. |
| `409` | `idempotency_key_reuse`, `stale_correction_round` | Use the original target or refresh the current step and round. |
| `422` | `unsupported_ats` | The target is not a supported public Ashby job URL; no charge is made. |
| `429` | `rate_limit_exceeded` | Wait for `Retry-After`, then replay exactly. |
| `502` | `upstream_unavailable` | Jobo could not be reached; replay safely. |
| `503` | `billing_pending`, `provider_catalog_unavailable` | Wait for `Retry-After` when present, then replay exactly. |
| `504` | `upstream_timeout` | Replay the same key or session token and body to re-attach. |

Jobo problem bodies and stable codes are forwarded faithfully. Do not blindly retry other `4xx` responses.

### Privacy and permissions

Responses contain the full versioned application: fields, accepted answers, correction errors, deadlines, and terminal failure. The shared Jobo service account's `owner_id` is always removed.

The Actor uses limited Apify permissions and a named Request Queue only as its billing ledger. It does **not** use Apify Proxy. API keys, answer bodies, file URLs, full application URLs, and session tokens are never logged. Operational logs contain only hashed user/session identifiers, route, latency, status, and stable error code.

The Actor's source files are hidden from the public Apify listing. The README, input/output schemas, OpenAPI definition, and non-sensitive build metadata remain public so callers can evaluate and integrate the API. The dedicated Jobo API key and publisher signing secret are stored as encrypted Apify secrets; their values are not included in public Actor, version, or build metadata.

### OpenAPI

The Standby API definition is published from [`.actor/web_server_openapi.json`](.actor/web_server_openapi.json). The internal `GET /` readiness probe required by Apify Standby is intentionally excluded from the public paths.

### Related links

| Link | Use it for |
|---|---|
| [Jobo service status](https://jobo.world/status) | Live availability and incident updates for the Jobo API behind this Actor |
| [Ashby Jobs Scraper](https://apify.com/jobo.world/ashby-jobs-scraper-api) | Find and filter live Ashby jobs before sending a supported application URL |
| [ATS Jobs API](https://apify.com/jobo.world/ats-jobs-api) | Search jobs across 150+ ATS platforms; filter to source `ashby` for targets supported by this release |
| [AI Deep Job Search](https://apify.com/jobo.world/ai-deep-job-search) | Discover and score matching jobs before beginning an application |
| [All Jobo Actors](https://apify.com/jobo.world) | Browse the complete Jobo catalogue on Apify |

# Actor input Schema

## `apply_url` (type: `string`):

Public Ashby job URL. Current formats: https://jobs.ashbyhq.com/{organization}/{job-uuid} or the same URL ending in /application.

## `idempotency_key` (type: `string`):

Stable key for safely re-attaching after an interrupted start. If omitted, the Actor run ID is used and returned with the result.

## Actor input object example

```json
{}
```

# Actor output Schema

## `initial_application` (type: `string`):

The session token, first questions or terminal result, next action, and effective idempotency key returned by a normal Actor run.

## `standby_api` (type: `string`):

Start an application with POST /v1/applications, then submit each complete answer snapshot with POST /v1/applications/answers.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("jobo.world/career-site-auto-apply").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("jobo.world/career-site-auto-apply").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call jobo.world/career-site-auto-apply --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jobo.world/career-site-auto-apply"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ox16i7mCKdmsSDnMx/builds/G2AqSCEwxagy20SGz/openapi.json
