Factorial API SDKs
Official SDKs that wrap the Factorial public API with a domain-namespaced client,
pagination helpers, and clean auth. This skill teaches an agent how to use them
and how to work with Factorial webhooks. Generated reference data lives in
reference/ (see References).
- TypeScript:
@factorialco/api-client(npm) - Python:
factorial-api-client(PyPI) - Ruby:
factorial_api(RubyGems)
Install
npm install @factorialco/api-client # TypeScript
pip install factorial-api-client # Python
gem install factorial_api # Ruby
Authentication
Provide either an API key or an OAuth token (or both):
| Option | Header sent |
|---|---|
apiKey |
x-api-key: <value> |
token |
Authorization: Bearer <value> |
The Factorial API key is JWT-formatted but is sent as
x-api-key, not as a bearer token.
Credentials can be passed explicitly or read from the environment
(FACTORIAL_API_KEY, FACTORIAL_TOKEN, optional FACTORIAL_BASE_URL).
import { FactorialClient } from "@factorialco/api-client";
const client = new FactorialClient({ apiKey: process.env.FACTORIAL_API_KEY });
from factorial_api_client import FactorialClient
client = FactorialClient(api_key="YOUR_KEY") # or token="...", or from env
require "factorial_api"
api = F::Api.new(api_key: ENV["FACTORIAL_API_KEY"]) # or token:, or from env
Calling endpoints
The client is organized as client.<namespace>.<resource>.<method>. Namespaces and
resources mirror the REST path /api/<version>/resources/<namespace>/<resource>.
Method naming is deterministic:
| HTTP / shape | SDK method |
|---|---|
GET collection |
list() / paginate() / all() |
GET by id |
get(id) |
POST collection |
create(body) |
PUT/PATCH by id |
update(id, body) |
DELETE by id |
delete(id) |
Custom action (e.g. apply) |
camelCased action, e.g. apply() |
const { data } = await client.employees.employees.list();
const created = await client.apiPublic.webhookSubscriptions.create({ /* body */ });
employees = client.employees.employee.list()
sub = client.api_public.webhook_subscription.create(body={...})
Ruby follows the same shape in snake_case, with required params — path,
query or form alike — as named keywords; optional query params go in a
query_params: hash and bodies under their *_request keyword. (The raw
low-level accessors, e.g.
api.employees_employee.employees_employees_get(true, false), keep working
underneath.)
employees = api.employees.employee.list(only_active: true, only_managers: false).data
team = api.teams.team.create(teams_teams_post_request: { name: "Platform" })
To discover the exact namespace/resource/method for any endpoint, consult
reference/sdk-methods.md (every endpoint with its TS and Ruby calls, grouped)
or the online API reference.
Pagination
List endpoints return { data: T[], meta: { end_cursor?, has_next_page, ... } }.
list()— one raw pagepaginate()— async iterator (TS) / sync or async generator (Python)all()(TS) /collect_all()(Python) — fetch every page into one array/list- Ruby:
paginate(lazy Enumerator, caps vialimit:/max_items:) andallon every listable resource;F::Api.paginate { |page| ... }wraps raw calls
for await (const emp of client.employees.employees.paginate()) { /* ... */ }
const everyone = await client.employees.employees.all({ maxItems: 500 });
for emp in client.employees.employee.paginate(max_items=50): ...
everyone = client.employees.employee.all()
High-volume retrieval
Pages are capped at 100 items — a server-side hard max, not a default
(see the pagination docs).
Passing a larger limit has no effect. Cursor pagination is sequential (page
N+1 needs page N's end_cursor), so all() on a big dataset (e.g. a full
company-month of attendance/worked_times) means dozens of sequential
requests. To keep request counts and latency sane:
- Filter first. Use the endpoint's query params (date ranges,
ids,employee_ids, …) instead of pulling everything and filtering locally. - Sync incrementally. Where an endpoint supports
updated_at-style filters, fetch only what changed since your last run and cache locally — don't re-pull the full dataset per report. - Shard big pulls. Split one large query into filtered sub-queries (by date window or id chunks) and run those concurrently — each sub-query still paginates sequentially, but wall-clock time drops. Total request count is unchanged, so watch rate limits.
- Cap defensively. Pass
max_items/maxItemsso a bug or unexpected data volume can't turn into an unbounded crawl.
There is no server-side aggregation endpoint; totals (e.g. hours per employee per period) must be computed client-side from the raw records.
Errors
The client throws on any non-2xx response (bad/expired token, wrong base URL, server errors) instead of silently returning empty data. Wrap calls in try/catch (TS) or try/except (Python).
API versioning
The SDK targets a specific Factorial API date version (e.g. 2026-04-01), pinned
by the SDK's major version. Newer API versions ship as new SDK majors.
Webhooks
Webhooks are HTTP POSTs Factorial sends to your target_url when an event happens.
How delivery works
- You create a webhook subscription for a
subscription_type(e.g.ats/application/create), pointing at atarget_url, with yourcompany_id. - When the event fires, Factorial sends a
POSTtotarget_url. The payload is the resource object at the top level — it is not wrapped in a{ type, data }envelope. Use a distinct URL per event if you need to tell them apart easily. - If you set a
challengewhen subscribing, Factorial echoes it back in thex-factorial-wh-challengerequest header so you can verify the source. - Optional author headers
x-factorial-author-id/x-factorial-author-type(employeeorcompany) identify who triggered the event, when safe to expose. - Retry policy: up to 20 retries over 48h; after that the subscription is
disabled and you are emailed. Re-enable with a
PUT { enabled: true }.
Manage subscriptions via the SDK
await client.apiPublic.webhookSubscriptions.create({
subscription_type: "ats/application/create",
target_url: "https://example.com/webhooks/factorial",
challenge: "a-secret-you-choose",
company_id: 55,
});
client.api_public.webhook_subscription.create(body={
"subscription_type": "ats/application/create",
"target_url": "https://example.com/webhooks/factorial",
"challenge": "a-secret-you-choose",
"company_id": 55,
})
Type the handler payload with the exported webhook types:
import type { AtsApplicationCreateWebhook, WebhookPayloadMap, WebhookSubscriptionType }
from "@factorialco/api-client";
// Direct alias
function onApplicationCreated(payload: AtsApplicationCreateWebhook) { /* ... */ }
// Typed dispatch keyed on the runtime subscription_type
function handle<T extends WebhookSubscriptionType>(type: T, payload: WebhookPayloadMap[T]) { /* ... */ }
from factorial_api_client import AtsApplicationCreateWebhook, WEBHOOK_PAYLOAD_TYPES
def on_application_created(payload: AtsApplicationCreateWebhook) -> None: ...
# Runtime lookup of the model class for a subscription_type
model_cls = WEBHOOK_PAYLOAD_TYPES["ats/application/create"]
The full list of events, their subscription_type, and payload fields is in
reference/webhooks.md.
References
Generated, SDK-specific lookup tables (consult these for exact names/shapes):
reference/webhooks.md— every webhook event, itssubscription_type, and payload fields.reference/sdk-methods.md— every REST endpoint grouped by namespace/resource, with the SDK call.
For prose guides (OAuth flows, API keys, versioning, webhook policies, etc.), use the live docs — always current, not vendored here:
- Getting started: https://apidoc.factorialhr.com/docs/getting-started
- Full API reference: https://apidoc.factorialhr.com/reference