← all tools
Recruiting CRM / ATS

Loxo

Push candidates into Loxo and attach them to the right job.

Cost per callBYOK · vendor billed
Your own keyrequired
CategoryRecruiting CRM / ATS

Loxo is where the work gets recorded. Your agent creates candidate records, reads your live jobs, and links people to the roles they are up for. These are real writes to a production system, so it does one and shows you before it does the rest.

Loxo is the destination. Everything the pipeline produced ends up filed here.

things you can ask for

  • Add these candidates and attach them to the job
  • What roles are open? Source a longlist for the hardest one with AI Ark
  • Is this person already in the system before I spend a credit on Lusha?
  • Attach this shortlist to the role and file the Granola notes

works well with Loxo

  • AI Ark

    source against the roles already open in Loxo

  • Lusha

    enrich only the candidates Loxo does not already hold

  • Granola

    meeting notes filed onto the candidate

  • HeyReach

    run LinkedIn outreach off a Loxo shortlist

Ask for the outcome and your agent composes the run itself. Or start from a skill, a whole pipeline it already knows end to end.

browse all skills →

What your agent reads before it touches Loxo

Every tool ships with a written playbook, and the agent loads it before the first call. Auth, rate limits, what each call costs, which actions need your approval, and the mistakes worth avoiding. It is the difference between an agent that knows the tool and one that guesses at it.

Read the Loxo docs →
Show the raw playbookwritten for the agent

HOW in lib/loxo.py.

Auth & config (agency-scoped)

  • Base URL: https://app.loxo.co/api/{LOXO_AGENCY_SLUG} — Loxo is agency-scoped. The host is fixed; an agency subdomain 404s.
  • Env: LOXO_API_KEY (Bearer), LOXO_AGENCY_SLUG (bare slug, e.g. acme — a pasted URL/domain is reduced to its first label). Never hardcode.
  • Paths CONFIRMED from loxo.readme.io (2026-06-01). Jobs paginate via page/per_page; people and companies paginate via scroll_id (+ a Lucene query). Index: reference/docs/loxo/raw/endpoints.md.

Operations

Jobs: list_jobs, get_job, create_job, update_job. People: list_people, get_person, create_person, update_person. Companies (accounts): list_companies, iter_companies, get_company, create_company, update_company, plus list_dynamic_fields + company_custom_fields for the agency's custom fields. Job↔candidate: list_job_candidates (GET /jobs/{id}/candidates), apply_to_job (POST /jobs/{id}/apply).

Notes & guardrail

  • Adding a candidate to a job is apply_to_job (POST /jobs/{job_id}/apply) — multipart/form-data with REQUIRED email+name+phone (+ optional resume file). create_job_candidate is a deprecated alias.
  • Production ATS writes — pilot one, confirm, then bulk.
  • list_companies paginates by cursor only: pass the scroll_id each response returns, with a per_page up to 100 — there is no page param. iter_companies(max_records=…) walks the whole account list.
  • A company's agency custom fields arrive as custom_hierarchy_1..6 {id, value} pairs, and each slot holds a different field per agency. Read the definitions once with list_dynamic_fields, then pass a {"custom_hierarchy_3": "Therapeutic area"} map to company_custom_fields(company, labels).

Handoff

ATS write target: enriched candidates → create_person / apply_to_job (assign to a job). Company-first BD: iter_companies → each account's url (website) → hyreflow_native career-page scraping for their open roles → contacts via the people_search waterfall.

Callable surface — lib/loxo.py

Import: from lib.loxo import Loxo → instantiate Loxo() (reads key from env). Generic passthrough: request(method, path, *, params, json).

  • apply_to_job(job_id: str, *, email: str, name: str, phone: str, resume: tuple | None = None, **fields) -> Any — POST /jobs/{job_id}/apply — add a candidate to a job (CONFIRMED).
  • company_custom_fields(company: dict, labels: dict | None = None) -> dict — Local (no HTTP): a company's custom_hierarchy_1..6 slots as {field_name: value}.
  • create_company(payload: dict) -> Any — POST /companies — create a company (form-encoded, company[...] params).
  • create_job(payload: dict) -> Any — POST /jobs — create a job.
  • create_job_candidate(job_id: str, payload: dict) -> Any — DEPRECATED alias → apply_to_job (POST /jobs/{id}/apply, multipart).
  • create_person(payload: dict) -> Any — POST /people — create a person. (Resume upload needs multipart via request().)
  • get_company(company_id: str) -> Any — GET /companies/{id} — one company, with its website (url) and custom fields.
  • get_job(job_id: str) -> Any — GET /jobs/{id}.
  • get_person(person_id: str) -> Any
  • iter_companies(*, per_page: int = 100, max_records: int = 1000, **params) -> Iterator[dict] — GET /companies — follow the scroll cursor until the account list is exhausted.
  • list_companies(**params) -> Any — GET /companies — search/list companies (accounts, employers).
  • list_dynamic_fields() -> Any — GET /dynamic_fields — the agency's custom (dynamic) field definitions.
  • list_job_candidates(job_id: str, **params) -> Any — GET /jobs/{job_id}/candidates — candidates on a job, with pipeline stage (CONFIRMED).
  • list_jobs(**params) -> Any — GET /jobs — paginated (page, per_page).
  • list_people(**params) -> Any — GET /people — paginated people/candidates.
  • update_company(company_id: str, payload: dict) -> Any — PUT /companies/{id} (form-encoded, company[...] params).
  • update_job(job_id: str, payload: dict) -> Any — PUT /jobs/{id}.
  • update_person(person_id: str, payload: dict) -> Any