Reference

API reference

Every public endpoint, with field-level documentation. Lists return { items, page, perPage, total }; resources return the object directly. Money is integer cents, timestamps are ISO 8601 in UTC.

Open the spec
Prefer machine-readable? The same data lives in our OpenAPI 3.1 spec at /v1/openapi.json.

Health

GET/v1/ping

Liveness probe. Always 200 unless the platform is unreachable.

json
{
  "ok": true,
  "time": "2026-05-03T08:14:22.103Z"
}

Companies

Operations on the organization the API key belongs to. There is no path-level org id; the key carries the binding.

GET/v1/companies/mecompany:read

Org identity, legal data, KYB status, active subscription plan.

Response

  • idstringrequired
    Opaque organization identifier.
  • namestringrequired
    Display name.
  • legalNamestring
    Registered legal name. Snapshotted onto invoices at issue time.
  • countryCodestring
    ISO 3166-1 alpha-2 country code.
  • taxIdstring
    Local tax identifier (e.g. NIF in Spain).
  • vatIdstring
    EU VAT id when applicable.
  • typestring
    company for orgs with employees / contractors; freelancer_solo for one-person shops.
    companyfreelancer_solo
  • kybStatusstring
    Outcome of the KYB review.
    pendingin_reviewapprovedrejected
  • planstring
    Active subscription plan key (trial, core, professional, global, enterprise, aor, custom_*).
  • subscriptionStatusstring
    Stripe subscription status.
    trialingactivepast_duecanceledunpaid
  • createdAtISO 8601required
    Org creation timestamp.
json
{
  "id": "org_2024_AbCd",
  "name": "Acme Corp",
  "legalName": "Acme Corporation S.L.",
  "countryCode": "ES",
  "taxId": "B12345678",
  "vatId": "ESB12345678",
  "type": "company",
  "kybStatus": "approved",
  "plan": "global",
  "subscriptionStatus": "active",
  "createdAt": "2024-08-12T09:31:24.000Z"
}
GET/v1/companies/me/statscompany:read

Quick aggregates: total / active projects, member count.

json
{
  "organizationId": "org_2024_AbCd",
  "totalProjects": 42,
  "activeProjects": 9,
  "memberCount": 14
}

Projects

Engagements and listings owned by the organization.

GET/v1/companies/me/projectsprojects:read

Paginated list of projects the org owns.

Query parameters

  • pageinteger
    1-indexed. Default 1.
  • perPageinteger
    1–100. Default 25.
  • statusstring
    Filter by lifecycle status.
  • searchstring
    Substring match against title (case-insensitive).
  • sortstring
    Sort key. Default -createdAt.
json
{
  "items": [
    {
      "id": "prj_2025_8f3a",
      "type": "internal",
      "title": "Mobile app rewrite",
      "status": "active",
      "currency": "EUR",
      "budgetCents": 4500000,
      "budgetHours": null,
      "skills": ["react-native", "typescript", "graphql"],
      "publishedAt": "2025-09-18T10:00:00.000Z",
      "createdAt": "2025-09-12T15:42:08.000Z"
    }
  ],
  "page": 1,
  "perPage": 25,
  "total": 9
}
GET/v1/companies/me/projects/:idprojects:read

Single project, full payload.

Response

  • idstringrequired
    Opaque project identifier.
  • typestringrequired
    marketplace = public listing accepting applications. internal = direct engagement with named members.
    marketplaceinternal
  • titlestringrequired
    Project title.
  • slugstring
    URL slug (only on marketplace projects).
  • statusstring
    Lifecycle status. The set of valid values depends on type.
    draftopenactiveon_holdclosedcancelledarchived
  • currencystring
    ISO 4217 currency code for budget figures.
  • budgetCentsinteger
    Project budget in cents. null when not set.
  • budgetHoursinteger
    Capped hours when applicable. null when not set.
  • skillsstring[]
    Skill slugs the project needs.
  • startDateISO 8601
    Engagement start (planned).
  • endDateISO 8601
    Engagement end (planned). null for ongoing.
  • publishedAtISO 8601
    When the project was opened to applications. null for unpublished drafts.
  • createdAtISO 8601required
    When the project was created.

Freelancers

People with at least one project membership in your organization.

GET/v1/companies/me/freelancersfreelancers:read

Distinct freelancers across all projects.

Query parameters

  • pageinteger
    1-indexed. Default 1.
  • perPageinteger
    1–100. Default 25.

Item shape

  • userIdstringrequired
    User identifier.
  • namestring
    Display name.
  • emailstring
    Primary email.
  • slugstring
    Public profile slug at /freelance/<slug>.
  • headlinestring
    One-line professional headline.
GET/v1/companies/me/freelancers/:userIdfreelancers:read

User + freelancer profile. Returns 404 if the freelancer has no project relationship with you.

json
{
  "user": {
    "id": "usr_a4ef",
    "name": "Marta López",
    "email": "marta@example.com",
    "isFreelancer": true
  },
  "profile": {
    "userId": "usr_a4ef",
    "slug": "marta-lopez",
    "headline": "Senior backend engineer · Go / Postgres"
  }
}
POST/v1/companies/me/freelancers/invitefreelancers:write

Adds a freelancer to your team so you can pay them. A new person gets an account and a sign-in email to finish onboarding (identity check, payout account, self-billing mandate); someone who is already on Worksible is attached silently, without an email. Use the returned freelancer.userId as freelancerUserId when you create payments.

Body

  • emailstringrequired
    Freelancer email. Idempotent on it: an existing user is attached to your team instead of duplicated.
  • namestringrequired
    Full name, 1–255 characters.
  • countryCodestring
    ISO 3166-1 alpha-2.
  • preferredCurrencystring
    ISO 4217. Default EUR.
  • phonestring
    Up to 20 characters.
  • externalIdstring
    Your own id for this person, kept on the team membership.
  • projectIdstring
    UUID of one of your projects: the freelancer is also added to it as a member.
bash
curl -X POST https://api.worksible.com/v1/companies/me/freelancers/invite \
  -H "Authorization: Bearer $WORKSIBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "marta@example.com",
    "name": "Marta López",
    "countryCode": "ES",
    "externalId": "contractor-8812"
  }'

201 Created for a new account, 200 OK when the email already existed (invitation.status is then skipped_existing_user).

json
{
  "freelancer": {
    "userId": "usr_a4ef",
    "email": "marta@example.com",
    "name": "Marta López",
    "createdAt": "2026-11-03T09:12:44.000Z",
    "externalId": "contractor-8812"
  },
  "invitation": {
    "status": "sent",
    "callbackUrl": "https://app.worksible.com/welcome?invited_by_org=org_2024_AbCd",
    "inviteUrl": null
  },
  "created": true
}
Errors
404 project_not_in_org when projectId is not one of yours. 400 for an invalid body (email format, missing name).

Invoices

Issued and received invoices, including consolidated MoR billing, self-billing and credit notes.

GET/v1/companies/me/invoicesinvoices:read

Paginated list. The org is matched as either issuer or recipient.

Query parameters

  • pageinteger
    1-indexed. Default 1.
  • perPageinteger
    1–100. Default 25.
  • statusstring
    Filter by status. Multiple values comma-separated.
  • typestring
    Filter by invoice type.

Item shape

  • idstringrequired
    Opaque invoice id.
  • typestringrequired
    Invoice category. consolidated = MoR billing; self_billing = platform paying freelancer.
    consolidatedself_billingsubscriptioncredit_noteproformalocal
  • statusstringrequired
    Lifecycle.
    draftissuedsentpaidoverduecancelledcredited
  • invoiceNumberstring
    Sequential issuer-side number. null while draft.
  • issuerOrganizationIdstring
    Issuer org. Snapshot of the issuer fields stays on the invoice forever.
  • recipientOrganizationIdstring
    Recipient org (when the recipient is a legal entity).
  • recipientUserIdstring
    Recipient user (for self-billing scenarios).
  • currencystringrequired
    ISO 4217.
  • subtotalCentsintegerrequired
    Pre-tax total.
  • taxAmountCentsintegerrequired
    Tax accumulated across breakdown rows.
  • totalCentsintegerrequired
    Customer-facing total (subtotal + tax).
  • paidCentsinteger
    Sum of payments received against this invoice.
  • taxModelstring
    Resolved tax rule for this invoice.
    standard_vatreverse_chargeexemptirnrexport
  • issueDateYYYY-MM-DD
    Issue date. null while draft.
  • dueDateYYYY-MM-DD
    Customer payment deadline.
  • paidAtISO 8601
    When the last reconciliation flipped status to paid.
  • downloadTokenstring
    Public token for /v1/invoices/public/:token download. null until issued.

Payments

Payments you owe your freelancers. You register each one as the work is done; Worksible bills you for the whole month in one consolidated invoice and, once that invoice is paid, pays every freelancer through their own payout account.

  1. Register with POST /v1/companies/me/payments (or the batch endpoint). The payment is created pending_billing. No money moves yet.
  2. Close the month.All the month's payments go into one consolidated invoice, generated from the Worksible app (Payments → Billing). There is no API endpoint for this step.
  3. Pay the invoice. Each payment moves to pending, then reserved while it is paid out, and paid when it reaches the freelancer.
Several currencies, one invoice in EUR
Register each payment in the currency you agreed with the freelancer (EUR, USD…). The monthly invoice is issued in EUR whatever the mix. Payments in another currency are converted when the cycle is closed: each freelancer at the exchange rate of the channel they are paid through, plus a small currency-hedging margin. The EUR total is shown before you confirm. The freelancer receives the equivalent of their amount at that rate, without the margin. Never mix currencies under one externalId: a payment is one amount in one currency.
GET/v1/companies/me/paymentspayments:read

Paginated list of the payments you registered, newest first.

Query parameters

  • pageinteger
    1-indexed. Default 1.
  • perPageinteger
    1–100. Default 25.
  • statusstring
    One status, exact match (e.g. pending_billing).

Item shape

  • idstringrequired
    Worksible payment id (UUID). Store it next to your externalId.
  • freelancerUserIdstringrequired
    Freelancer being paid.
  • amountCentsintegerrequired
    What the freelancer receives, in currency. With a supplier commission this is the NET; the gross you sent is supplierFee.grossCents.
  • currencystringrequired
    ISO 4217 code the payment was registered in (EUR, USD…). It never changes.
  • descriptionstring
    Concept as sent on creation. Appears on the freelancer's self-billing invoice.
  • statusstringrequired
    pending_billing = registered, waiting for the monthly invoice. pending = on an invoice, waiting for your payment. reserved / scheduled = being paid out. paid = with the freelancer.
    pending_billingpendingreservedscheduledpaidfailedcancelled
  • externalIdstring
    Your own identifier. Unique per organization; the idempotency key of creation.
  • supplierFeeobject
    Only with a supplier commission: { grossCents, feeCents, netCents, platformCents, companyCents, totalBps, platformBps, acceptanceRequired }, all in currency. null otherwise.
  • billingCycleYYYY-MM
    Month whose consolidated invoice will bill this payment (UTC month of creation).
  • invoiceIdstring
    Consolidated invoice that billed it. null while pending_billing.
  • createdAtISO 8601required
    Registration time.
  • updatedAtISO 8601required
    Last status change.
json
{
  "items": [
    {
      "id": "8b1f2c4e-6d0a-4f3b-9e21-5a7c3d9e1f40",
      "freelancerUserId": "usr_a4ef",
      "amountCents": 97100,
      "currency": "USD",
      "description": "Content production, October 2026",
      "status": "pending_billing",
      "externalId": "acme-payment-12345",
      "supplierFee": {
        "grossCents": 100000,
        "feeCents": 2900,
        "netCents": 97100,
        "platformCents": 2900,
        "companyCents": 0,
        "totalBps": 290,
        "platformBps": 290,
        "acceptanceRequired": false
      },
      "billingCycle": "2026-11",
      "invoiceId": null,
      "createdAt": "2026-11-03T09:20:11.000Z",
      "updatedAt": "2026-11-03T09:20:11.000Z"
    }
  ],
  "page": 1,
  "perPage": 25,
  "total": 1
}
GET/v1/companies/me/payments/:idpayments:read

One payment by the id Worksible returned on creation. Returns { payment } with the same shape as a list item, or 404 not_found when it is not one of yours.

GET/v1/companies/me/payments/by-external/:externalIdpayments:read

One payment by your own externalId. Use it to reconcile after a timeout before deciding whether to retry. Returns { payment } or 404 not_found.

POST/v1/companies/me/paymentspayments:write

Registers what you owe one freelancer for one piece of work. Creating it does not transfer money: it waits for the monthly invoice. The freelancer must be in your team (see Invite a freelancer).

Body

  • freelancerUserIdstringrequired
    userId returned by Invite a freelancer (or by self-registration). A user who exists on Worksible but is not in your team yet is attached automatically; an unknown id returns 404.
  • amountCentsintegerrequired
    Positive integer in the currency's minor unit. With a supplier commission, the GROSS amount, before the freelancer's commission.
  • currencystringrequired
    ISO 4217, three letters (EUR, USD…). Gross, commission and net are computed in this currency; there is no conversion at registration.
  • descriptionstringrequired
    1–500 characters. The concept of the work being paid.
  • externalIdstring
    Up to 255 characters, unique per organization. Required with a supplier commission. Reuse it on every retry of the same payment, never for another one.
  • amountModestring
    Required when your organization has a supplier commission configured: amountCents is then the gross. Do not send it otherwise.
    gross
  • projectIdstring
    UUID of one of your projects.
  • legalOrderIdstring
    UUID of the signed order (engagement) this payment belongs to, when you manage them.
  • metadataobject
    Your own reconciliation data. Stored under metadata.custom; it can never overwrite amounts, fees or identifiers.
bash
curl -X POST https://api.worksible.com/v1/companies/me/payments \
  -H "Authorization: Bearer $WORKSIBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "freelancerUserId": "usr_a4ef",
    "amountCents": 100000,
    "amountMode": "gross",
    "currency": "USD",
    "description": "Content production, October 2026",
    "externalId": "acme-payment-12345"
  }'

201 Created for a new payment. The same externalId again returns 200 OK with "idempotent": true and the original payment: nothing is created twice.

json
{
  "payment": {
    "id": "8b1f2c4e-6d0a-4f3b-9e21-5a7c3d9e1f40",
    "freelancerUserId": "usr_a4ef",
    "amountCents": 97100,
    "currency": "USD",
    "status": "pending_billing",
    "externalId": "acme-payment-12345",
    "supplierFee": { "grossCents": 100000, "feeCents": 2900, "netCents": 97100, "...": "..." },
    "billingCycle": "2026-11",
    "invoiceId": null
  },
  "idempotent": false,
  "payoutReadiness": {
    "canReceivePayments": false,
    "blockers": ["kyc_not_verified"],
    "kycStatus": "pending",
    "hasSelfBillingMandate": true,
    "hasPayoutMethod": true,
    "hasDefaultPayoutMethod": true,
    "payoutCurrencies": ["EUR"]
  }
}

payoutReadiness

  • canReceivePaymentsbooleanrequired
    The bottom line: money can be paid out to this freelancer today.
  • blockersstring[]required
    Why not, when canReceivePayments is false. The payment is registered anyway and waits.
    kyc_not_verifiedself_billing_mandate_missingno_payout_method
  • kycStatusstring
    pending | in_review | verified | rejected | expired, or null when never started.
  • hasSelfBillingMandatebooleanrequired
    The freelancer signed the self-billing mandate.
  • hasPayoutMethodbooleanrequired
    At least one active payout account.
  • hasDefaultPayoutMethodbooleanrequired
    A default payout account is set (required to pay out).
  • payoutCurrenciesstring[]required
    Currencies the freelancer can be paid in.

The payment is registered even when the freelancer cannot be paid yet: the earning is real. Show the blockers to whoever triggered the payment. Organizations without a supplier commission also get a warning object (freelancer_cannot_receive_payments) in that case.

Supplier commission: send the gross
When your organization has a supplier commission (a percentage deducted from the freelancer's payment), amountMode: "gross" and externalId are mandatory and amountCents is the gross. Do not deduct the commission yourself, and do not add it: Worksible computes it from your configuration. The response amountCents is the net; reconcile your side with supplierFee.grossCents. The percentages are never sent in the request.

Errors

  • freelancer_not_found404 not_found
    Unknown freelancerUserId (in reason). Invite the freelancer first.
  • project_not_in_org404 not_found
    projectId is not one of your projects (in reason).
  • gross_amount_required409 supplier_fee_parameters_required
    Your organization has a supplier commission: add amountMode: "gross".
  • external_id_required409 supplier_fee_parameters_required
    Add a stable externalId.
  • no_active_supplier_fee_policy409
    You sent amountMode: "gross" but your organization has no supplier commission. Remove it.
  • mixed_currencies409
    One payment carries one currency.
  • quote_expired409
    Retry with the same externalId: the server prices it again.
  • supplier_acceptance_required409
    The freelancer has to accept the commission conditions first (only when your configuration requires it).
  • payment_recipient_merge_requires_review409
    That freelancer account was merged into another one and needs a manual check. Contact support.
  • invalid_currency422 supplier_fee_currency_unsupported
    Send a three-letter ISO 4217 code.
  • registration_failed500
    Retry with the same externalId; it is safe.
Retries
A timeout is not proof that nothing was registered. On a 5xx or a lost connection, repeat the exact request with the same externalId, or look it up with Get by external id. On a 429, wait for Retry-After. Never generate a new externalId for a retry, and never reuse one for a different freelancer or amount.
POST/v1/companies/me/payments/batchpayments:write

Registers 1 to 100 payments in one call, each with exactly the body of Create a payment and its own externalId. Rows are independent: some can succeed while others fail, and the successes are not rolled back. In a batch the freelancers must already be in your team.

json
{
  "payments": [
    {
      "freelancerUserId": "usr_a4ef",
      "amountCents": 100000,
      "amountMode": "gross",
      "currency": "USD",
      "description": "Content production, October 2026",
      "externalId": "acme-payment-12345"
    },
    {
      "freelancerUserId": "usr_77b2",
      "amountCents": 45000,
      "amountMode": "gross",
      "currency": "EUR",
      "description": "Content production, October 2026",
      "externalId": "acme-payment-12346"
    }
  ]
}

Always 201 Created: check summary.failed and errors. summary.created also counts rows resolved by an existing externalId (idempotent: true). Each row of payments carries its index in your request.

json
{
  "summary": { "created": 1, "failed": 1 },
  "payments": [
    {
      "index": 0,
      "id": "8b1f2c4e-6d0a-4f3b-9e21-5a7c3d9e1f40",
      "freelancerUserId": "usr_a4ef",
      "amountCents": 97100,
      "currency": "USD",
      "status": "pending_billing",
      "externalId": "acme-payment-12345",
      "supplierFee": { "grossCents": 100000, "feeCents": 2900, "netCents": 97100, "...": "..." },
      "billingCycle": "2026-11",
      "idempotent": false
    }
  ],
  "errors": [
    { "index": 1, "freelancerUserId": "usr_77b2", "error": "freelancer_not_in_org" }
  ]
}

Row errors use the same codes as the single endpoint, plus freelancer_not_in_org (invite them first).

Self-registration

Public endpoints, without an API key, for freelancers who join your team on their own through your registration link. Call them from a browser or your backend; they never expose your key.

GET/v1/join/:code

The company behind a join code and the fields the registration form needs. 404 invalid_join_code when the code was altered.

json
{
  "company": { "name": "Acme Corp" },
  "registrationFields": {
    "email": { "required": true, "type": "email" },
    "name": { "required": true, "type": "string" },
    "countryCode": { "required": false, "type": "string", "description": "ISO 3166-1 alpha-2" },
    "preferredCurrency": { "required": false, "type": "string", "default": "EUR" },
    "phone": { "required": false, "type": "string" }
  }
}
POST/v1/join/:code

Registers the freelancer, adds them to your team and emails a sign-in link to finish onboarding. Body: email and name (required), countryCode, preferredCurrency, phone. Someone who already has an account is added to your team and gets the sign-in link again.

json
{
  "freelancer": { "userId": "usr_a4ef", "email": "marta@example.com", "name": "Marta López" },
  "message": "Registration accepted. Check your email to complete sign-in.",
  "created": true
}
POST/v1/auth/resend-invitation

Sends a new sign-in link to a freelancer whose invitation expired. Body: { email, organizationId }. Always answers { "status": "sent" }, whether or not the email exists, so it cannot be used to discover accounts.

More endpoints coming
Contracts, timesheets and payouts surfaces ship alongside webhooks. Subscribe to the changelog to be notified.