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.
Health
Liveness probe. Always 200 unless the platform is unreachable.
{
"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.
Org identity, legal data, KYB status, active subscription plan.
Response
idstringrequiredOpaque organization identifier.namestringrequiredDisplay name.legalNamestringRegistered legal name. Snapshotted onto invoices at issue time.countryCodestringISO 3166-1 alpha-2 country code.taxIdstringLocal tax identifier (e.g. NIF in Spain).vatIdstringEU VAT id when applicable.typestringcompany for orgs with employees / contractors; freelancer_solo for one-person shops.companyfreelancer_solokybStatusstringOutcome of the KYB review.pendingin_reviewapprovedrejectedplanstringActive subscription plan key (trial, core, professional, global, enterprise, aor, custom_*).subscriptionStatusstringStripe subscription status.trialingactivepast_duecanceledunpaidcreatedAtISO 8601requiredOrg creation timestamp.
{
"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"
}Quick aggregates: total / active projects, member count.
{
"organizationId": "org_2024_AbCd",
"totalProjects": 42,
"activeProjects": 9,
"memberCount": 14
}A permanent, signed link your freelancers can open to join your team on their own, plus the endpoints to build the same flow in your product (a “Connect with Worksible” button). The link does not expire and carries no API key.
{
"joinCode": "b3JnXzIwMjRfQWJDZA.4f1c9a7e02b3d6a1",
"companyName": "Acme Corp",
"registrationUrl": "https://app.worksible.com/join/b3JnXzIwMjRfQWJDZA.4f1c9a7e02b3d6a1",
"apiEndpoints": {
"inviteFreelancer": "https://api.worksible.com/v1/companies/me/freelancers/invite",
"getJoinInfo": "https://api.worksible.com/v1/join/b3JnXzIwMjRfQWJDZA.4f1c9a7e02b3d6a1",
"selfRegister": "https://api.worksible.com/v1/join/b3JnXzIwMjRfQWJDZA.4f1c9a7e02b3d6a1"
},
"flow": "Send freelancers to registrationUrl or invite them via API. They receive a magic-link email to complete onboarding."
}Projects
Engagements and listings owned by the organization.
Paginated list of projects the org owns.
Query parameters
pageinteger1-indexed. Default 1.perPageinteger1–100. Default 25.statusstringFilter by lifecycle status.searchstringSubstring match against title (case-insensitive).sortstringSort key. Default -createdAt.
{
"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
}Single project, full payload.
Response
idstringrequiredOpaque project identifier.typestringrequiredmarketplace = public listing accepting applications. internal = direct engagement with named members.marketplaceinternaltitlestringrequiredProject title.slugstringURL slug (only on marketplace projects).statusstringLifecycle status. The set of valid values depends on type.draftopenactiveon_holdclosedcancelledarchivedcurrencystringISO 4217 currency code for budget figures.budgetCentsintegerProject budget in cents. null when not set.budgetHoursintegerCapped hours when applicable. null when not set.skillsstring[]Skill slugs the project needs.startDateISO 8601Engagement start (planned).endDateISO 8601Engagement end (planned). null for ongoing.publishedAtISO 8601When the project was opened to applications. null for unpublished drafts.createdAtISO 8601requiredWhen the project was created.
Freelancers
People with at least one project membership in your organization.
Distinct freelancers across all projects.
Query parameters
pageinteger1-indexed. Default 1.perPageinteger1–100. Default 25.
Item shape
userIdstringrequiredUser identifier.namestringDisplay name.emailstringPrimary email.slugstringPublic profile slug at /freelance/<slug>.headlinestringOne-line professional headline.
User + freelancer profile. Returns 404 if the freelancer has no project relationship with you.
{
"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"
}
}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
emailstringrequiredFreelancer email. Idempotent on it: an existing user is attached to your team instead of duplicated.namestringrequiredFull name, 1–255 characters.countryCodestringISO 3166-1 alpha-2.preferredCurrencystringISO 4217. Default EUR.phonestringUp to 20 characters.externalIdstringYour own id for this person, kept on the team membership.projectIdstringUUID of one of your projects: the freelancer is also added to it as a member.
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).
{
"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
}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.
Paginated list. The org is matched as either issuer or recipient.
Query parameters
pageinteger1-indexed. Default 1.perPageinteger1–100. Default 25.statusstringFilter by status. Multiple values comma-separated.typestringFilter by invoice type.
Item shape
idstringrequiredOpaque invoice id.typestringrequiredInvoice category. consolidated = MoR billing; self_billing = platform paying freelancer.consolidatedself_billingsubscriptioncredit_noteproformalocalstatusstringrequiredLifecycle.draftissuedsentpaidoverduecancelledcreditedinvoiceNumberstringSequential issuer-side number. null while draft.issuerOrganizationIdstringIssuer org. Snapshot of the issuer fields stays on the invoice forever.recipientOrganizationIdstringRecipient org (when the recipient is a legal entity).recipientUserIdstringRecipient user (for self-billing scenarios).currencystringrequiredISO 4217.subtotalCentsintegerrequiredPre-tax total.taxAmountCentsintegerrequiredTax accumulated across breakdown rows.totalCentsintegerrequiredCustomer-facing total (subtotal + tax).paidCentsintegerSum of payments received against this invoice.taxModelstringResolved tax rule for this invoice.standard_vatreverse_chargeexemptirnrexportissueDateYYYY-MM-DDIssue date. null while draft.dueDateYYYY-MM-DDCustomer payment deadline.paidAtISO 8601When the last reconciliation flipped status to paid.downloadTokenstringPublic 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.
- Register with
POST /v1/companies/me/payments(or the batch endpoint). The payment is createdpending_billing. No money moves yet. - 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.
- Pay the invoice. Each payment moves to
pending, thenreservedwhile it is paid out, andpaidwhen it reaches the freelancer.
externalId: a payment is one amount in one currency.Paginated list of the payments you registered, newest first.
Query parameters
pageinteger1-indexed. Default 1.perPageinteger1–100. Default 25.statusstringOne status, exact match (e.g. pending_billing).
Item shape
idstringrequiredWorksible payment id (UUID). Store it next to your externalId.freelancerUserIdstringrequiredFreelancer being paid.amountCentsintegerrequiredWhat the freelancer receives, in currency. With a supplier commission this is the NET; the gross you sent is supplierFee.grossCents.currencystringrequiredISO 4217 code the payment was registered in (EUR, USD…). It never changes.descriptionstringConcept as sent on creation. Appears on the freelancer's self-billing invoice.statusstringrequiredpending_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_billingpendingreservedscheduledpaidfailedcancelledexternalIdstringYour own identifier. Unique per organization; the idempotency key of creation.supplierFeeobjectOnly with a supplier commission: { grossCents, feeCents, netCents, platformCents, companyCents, totalBps, platformBps, acceptanceRequired }, all in currency. null otherwise.billingCycleYYYY-MMMonth whose consolidated invoice will bill this payment (UTC month of creation).invoiceIdstringConsolidated invoice that billed it. null while pending_billing.createdAtISO 8601requiredRegistration time.updatedAtISO 8601requiredLast status change.
{
"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
}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.
One payment by your own externalId. Use it to reconcile after a timeout before deciding whether to retry. Returns { payment } or 404 not_found.
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
freelancerUserIdstringrequireduserId 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.amountCentsintegerrequiredPositive integer in the currency's minor unit. With a supplier commission, the GROSS amount, before the freelancer's commission.currencystringrequiredISO 4217, three letters (EUR, USD…). Gross, commission and net are computed in this currency; there is no conversion at registration.descriptionstringrequired1–500 characters. The concept of the work being paid.externalIdstringUp to 255 characters, unique per organization. Required with a supplier commission. Reuse it on every retry of the same payment, never for another one.amountModestringRequired when your organization has a supplier commission configured: amountCents is then the gross. Do not send it otherwise.grossprojectIdstringUUID of one of your projects.legalOrderIdstringUUID of the signed order (engagement) this payment belongs to, when you manage them.metadataobjectYour own reconciliation data. Stored under metadata.custom; it can never overwrite amounts, fees or identifiers.
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.
{
"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
canReceivePaymentsbooleanrequiredThe bottom line: money can be paid out to this freelancer today.blockersstring[]requiredWhy not, when canReceivePayments is false. The payment is registered anyway and waits.kyc_not_verifiedself_billing_mandate_missingno_payout_methodkycStatusstringpending | in_review | verified | rejected | expired, or null when never started.hasSelfBillingMandatebooleanrequiredThe freelancer signed the self-billing mandate.hasPayoutMethodbooleanrequiredAt least one active payout account.hasDefaultPayoutMethodbooleanrequiredA default payout account is set (required to pay out).payoutCurrenciesstring[]requiredCurrencies 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.
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_foundUnknown freelancerUserId (in reason). Invite the freelancer first.project_not_in_org404 not_foundprojectId is not one of your projects (in reason).gross_amount_required409 supplier_fee_parameters_requiredYour organization has a supplier commission: add amountMode: "gross".external_id_required409 supplier_fee_parameters_requiredAdd a stable externalId.no_active_supplier_fee_policy409You sent amountMode: "gross" but your organization has no supplier commission. Remove it.mixed_currencies409One payment carries one currency.quote_expired409Retry with the same externalId: the server prices it again.supplier_acceptance_required409The freelancer has to accept the commission conditions first (only when your configuration requires it).payment_recipient_merge_requires_review409That freelancer account was merged into another one and needs a manual check. Contact support.invalid_currency422 supplier_fee_currency_unsupportedSend a three-letter ISO 4217 code.registration_failed500Retry with the same externalId; it is safe.
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.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.
{
"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.
{
"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.
The company behind a join code and the fields the registration form needs. 404 invalid_join_code when the code was altered.
{
"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" }
}
}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.
{
"freelancer": { "userId": "usr_a4ef", "email": "marta@example.com", "name": "Marta López" },
"message": "Registration accepted. Check your email to complete sign-in.",
"created": true
}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.