Servicejoy REST API
Integrate your systems with Servicejoy to create clients, submit orders, generate invoices, and pull payment records — all programmatically.
Overview
All API endpoints are served from your Servicejoy account domain. Replace {your-subdomain} with the subdomain provided in your API settings.
https://{your-subdomain}.servicejoy.com
Request Format
All POST request bodies must be JSON with the header Content-Type: application/json, except the token endpoint which uses form-encoded data.
Response Format
All responses are JSON. Successful responses return HTTP 200 with the result object. Errors return a non-2xx status with a JSON error body.
Authentication
The API uses the OAuth 2.0 client_credentials flow. Obtain your client_id and client_secret from Settings → API inside your Servicejoy account.
Exchange your credentials for a JWT access token. Tokens expire after 30 minutes. Re-authenticate before making subsequent calls.
Request
Content-Type: application/x-www-form-urlencoded
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | required | Must be client_credentials |
client_id | string | required | Your API client ID (your account subdomain) |
client_secret | string | required | Your API secret key |
curl -X POST https://{your-subdomain}.servicejoy.com/api/token \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_SECRET"
import requests response = requests.post( "https://{your-subdomain}.servicejoy.com/api/token", data={ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_SECRET" } ) token = response.json()["access_token"]
const res = await fetch("https://{your-subdomain}.servicejoy.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_SECRET" }); const { access_token } = await res.json();
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 1800
}
Authorization: Bearer <access_token>Error Handling
| Status | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Missing required field or invalid value |
401 | Unauthorized | Missing or expired access token |
403 | Forbidden | Token does not have access to this resource |
404 | Not Found | Resource does not exist |
500 | Server Error | Unexpected server-side error |
Error responses include a JSON body:
{
"error": "Unauthorized",
"message": "Invalid or expired access token"
}
Clients
Manage your client organizations. Each client may have multiple contacts.
Returns all active clients for your account, ordered by name.
curl https://{your-subdomain}.servicejoy.com/api/Clients \ -H "Authorization: Bearer ACCESS_TOKEN"
Response — array of Client objects
[
{
"client_org_id": 42,
"name": "Acme Corp",
"currency": "USD",
"phone": "555-123-4567",
"fax": null,
"country": "United States",
"street1": "123 Main St",
"street2": null,
"city": "Springfield",
"stateProvince": "IL",
"postalCode": "62701",
"notes": null,
"state": 1,
"contacts": [
{
"contact_id": 101,
"client_org_id": 42,
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@acme.com",
"home_phone": null,
"mobile_phone": "555-987-6543",
"enable_login": true,
"login": "jane.doe@acme.com",
"invited": true
}
]
}
]
| Parameter | Type | Description |
|---|---|---|
id | integer (path) | The client_org_id |
curl https://{your-subdomain}.servicejoy.com/api/Clients/42 \ -H "Authorization: Bearer ACCESS_TOKEN"
Returns a single Client object (same schema as list) or 404 if not found.
Pass client_org_id: 0 (or omit it) to create a new client. Pass an existing ID to update.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_org_id | integer | optional | Omit or set to 0 for new client |
name | string | required | Company / client name |
currency | string | optional | ISO currency code, e.g. "USD" |
phone | string | optional | |
fax | string | optional | |
country | string | optional | Country name, 2-letter, or 3-letter code |
street1 | string | optional | |
street2 | string | optional | |
city | string | optional | |
stateProvince | string | optional | |
postalCode | string | optional | |
notes | string | optional | |
contacts | array | optional | Array of contact objects (see below) |
Contact Object
| Field | Type | Description |
|---|---|---|
contact_id | integer | Omit or 0 to add new contact |
first_name | string | |
last_name | string | |
email | string | Used for portal login & email notifications |
mobile_phone | string | |
enable_login | boolean | Allow contact to log into the client portal |
curl -X POST https://{your-subdomain}.servicejoy.com/api/Clients \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corp", "currency": "USD", "phone": "555-123-4567", "country": "US", "street1": "123 Main St", "city": "Springfield", "stateProvince": "IL", "postalCode": "62701", "contacts": [ { "first_name": "Jane", "last_name": "Doe", "email": "jane.doe@acme.com", "enable_login": true } ] }'
Returns the saved Client object.
Invoices
Query Parameters
| Parameter | Type | Description |
|---|---|---|
start | string | Start date filter, e.g. 1/1/2024 or 2024-01-01 |
end | string | End date filter |
invoice_number | string | Partial match on invoice number |
client_name | string | Partial match on client name (case-insensitive) |
curl "https://{your-subdomain}.servicejoy.com/api/Invoices?start=1/1/2024&end=12/31/2024" \ -H "Authorization: Bearer ACCESS_TOKEN"
Response — array of Invoice objects
[
{
"invoice_id": 1001,
"client_name": "Acme Corp",
"currency": "USD",
"order_id": 55,
"order_number": "ORD-000055",
"invoice_number": "INV-2024-001",
"invoice_date": "1/15/2024",
"invoice_due": "2/15/2024",
"total": 1500.00,
"outstanding": 1500.00,
"status": "Sent",
"invoiceLines": [
{
"item_title": "Web Design",
"descr": "Homepage redesign",
"cost": 1500.00,
"qty": 1,
"tax1_rate": null,
"is_header": false
}
]
}
]
Creates an invoice for an existing client (matched by name). If no client with that name exists, a new one is created. An email notification with a payment link is sent to the client's primary contact.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_name | string | required | Client name (case-insensitive match or new) |
currency | string | optional | ISO code, defaults to USD |
invoice_number | string | optional | If omitted, the next number in your account's invoice numbering sequence (the same numbering the app uses) |
invoice_date | string | optional | Date string, defaults to today |
invoice_due | string | optional | Due date, defaults to 30 days from today |
po_number | string | optional | Purchase order reference |
terms | string | optional | |
notes | string | optional | |
discount | decimal | optional | Discount amount or percentage |
discount_par | integer | optional | 1 = percentage, 0 = fixed amount |
stripe | boolean | optional | Enable Stripe payment button |
paypal | boolean | optional | Enable PayPal payment button |
code_field | string | optional | Custom reference / tracking code |
gclid | string | optional | Google Click ID for ad tracking |
invoiceLines | array | required | Array of line item objects (see below) |
Invoice Line Object
| Field | Type | Description |
|---|---|---|
item_title | string | Line item title |
descr | string | Description |
cost | decimal | Unit price |
qty | decimal | Quantity |
tax1_rate | decimal | First tax rate (%) |
tax1_name | string | First tax label, e.g. "GST" |
tax2_rate | decimal | Second tax rate (%) |
tax2_name | string | Second tax label |
is_header | boolean | If true, this is a section header (no price) |
curl -X POST https://{your-subdomain}.servicejoy.com/api/Invoices \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_name": "Acme Corp", "currency": "USD", "invoice_date": "1/15/2024", "invoice_due": "2/15/2024", "terms": "Net 30", "stripe": true, "invoiceLines": [ { "item_title": "Web Design", "descr": "Homepage redesign", "cost": 1500.00, "qty": 1 }, { "item_title": "SEO Setup", "cost": 500.00, "qty": 1, "tax1_rate": 8.5, "tax1_name": "Tax" } ] }'
Creates a project (order) and an attached invoice in a single call. Returns the full input object with payment_link populated — a direct URL your customer can use to view and pay the invoice.
| Field | Type | Description |
|---|---|---|
invoice_number | string | Invoice number to target |
state | integer | 0 = Deleted, 1 = Active, 2 = Archived |
curl -X POST https://{your-subdomain}.servicejoy.com/api/Invoices/ChangeInvoiceState \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "invoice_number": "API-000001", "state": 2 }'
Returns 200 OK on success.
Payments
Query Parameters
| Parameter | Type | Description |
|---|---|---|
start | string | Payment date from |
end | string | Payment date to |
invoice_number | string | Partial match on invoice number |
client_name | string | Partial match on client name |
curl "https://{your-subdomain}.servicejoy.com/api/Payments?start=1/1/2024" \ -H "Authorization: Bearer ACCESS_TOKEN"
Response — array of Payment objects
[
{
"paymentId": 501,
"invoice": "INV-2024-001",
"invoiceTotal": 1500.00,
"invoiceStatus": "Paid",
"codeField": null,
"client": "Acme Corp",
"email": "jane.doe@acme.com",
"project": null,
"agent": null,
"createdBy": null,
"paymentMethod": "Credit Card",
"notes": null,
"date": "2/1/2024",
"amount": 1500.00,
"currency": "USD",
"paymentGateway": "Stripe",
"transactionId": "ch_3OxABC1234",
"transactionDateUTC": "2024-02-01T14:22:11.0000000Z",
"onlinePaymentIPAddress": "192.0.2.1",
"terms": "Net 30"
}
]
Useful for incremental sync. Store the last PaymentId you processed and pass it here to retrieve only new payments.
curl https://{your-subdomain}.servicejoy.com/api/Payments/from/500 \ -H "Authorization: Bearer ACCESS_TOKEN"
Returns payments ordered by PaymentId ascending (same schema as list).
Submission
High-level endpoints that create client, order, and invoice in a single call. These are the most common integration points. All return a payment_link — a unique URL the client can open to view and pay their invoice.
The most complete submission endpoint. Pass a client, order, and invoice together. The client is matched by name (or created). The system emails the client a payment link and returns it in the response.
Request Body
| Field | Type | Description |
|---|---|---|
client | object | Client object (see Clients section) |
order | object | Order object (see below). Can be omitted. |
invoice | object | Invoice object (see Invoices section) |
Order Object
| Field | Type | Description |
|---|---|---|
title | string | Order / project title |
descr | string | Description |
status | integer | Status code (0 = new) |
deadline | string (ISO date) | Optional deadline |
client_visible | boolean | Show in client portal |
curl -X POST https://{your-subdomain}.servicejoy.com/api/Submission/SubmitOrder \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client": { "name": "Acme Corp", "phone": "555-123-4567", "country": "US", "contacts": [ { "first_name": "Jane", "last_name": "Doe", "email": "jane.doe@acme.com", "enable_login": true } ] }, "order": { "title": "Website Redesign Project", "descr": "Full site overhaul", "status": 0, "client_visible": true }, "invoice": { "currency": "USD", "invoice_date": "1/15/2024", "invoice_due": "2/15/2024", "stripe": true, "invoiceLines": [ { "item_title": "Web Design", "cost": 2000.00, "qty": 1 }, { "item_title": "Hosting Setup", "cost": 150.00, "qty": 1 } ] } }'
Response
{
"client": { /* saved client object */ },
"order": {
"order_id": 55,
"order_number": "ORD-000055",
"title": "Website Redesign Project"
},
"invoice": {
"invoice_id": 1001,
"invoice_number": "API-001001",
"total": 2150.00,
"status": "Sent"
},
"payment_link": "https://acme.servicejoy.com/External/ExtAccess/view/a3f9e2b1..."
}
Same as SubmitOrder but without creating a project/order. Use this when you just need an invoice.
curl -X POST https://{your-subdomain}.servicejoy.com/api/Submission/SubmitInvoice \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client": { "name": "Acme Corp", "contacts": [{ "first_name": "Jane", "email": "jane@acme.com" }] }, "invoice": { "currency": "USD", "stripe": true, "invoiceLines": [ { "item_title": "Consulting", "cost": 250.00, "qty": 4 } ] } }'
Returns same structure as SubmitOrder (with payment_link, without order).
Token Management
| Parameter | Type | Description |
|---|---|---|
id | string (query) | The refresh token ID to revoke |
curl -X DELETE "https://{your-subdomain}.servicejoy.com/api/RefreshTokens?id=TOKEN_ID" \ -H "Authorization: Bearer ACCESS_TOKEN"
Returns 204 No Content on success, 404 if token not found.
Complete Integration Example
The following Python example shows a full end-to-end flow: authenticate, submit an order, and retrieve the payment link.
import requests BASE_URL = "https://acme.servicejoy.com" # 1. Authenticate token_res = requests.post(f"{BASE_URL}/api/token", data={ "grant_type": "client_credentials", "client_id": "acme", "client_secret": "YOUR_SECRET_HERE" }) token_res.raise_for_status() access_token = token_res.json()["access_token"] headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } # 2. Submit an order with invoice order_res = requests.post(f"{BASE_URL}/api/Submission/SubmitOrder", headers=headers, json={ "client": { "name": "ACME Corp", "contacts": [{ "first_name": "Jane", "last_name": "Doe", "email": "jane@acme.com", "enable_login": True }] }, "order": { "title": "Q1 Service Contract", "status": 0 }, "invoice": { "currency": "USD", "stripe": True, "invoiceLines": [ {"item_title": "Monthly Retainer", "cost": 1200.00, "qty": 1}, {"item_title": "Extras", "cost": 350.00, "qty": 1} ] } } ) order_res.raise_for_status() result = order_res.json() print("Invoice:", result["invoice"]["invoice_number"]) print("Total:", result["invoice"]["total"]) print("Payment link:", result["payment_link"]) # 3. Poll for new payments (incremental sync) last_payment_id = 0 # store and persist this between runs payments_res = requests.get( f"{BASE_URL}/api/Payments/from/{last_payment_id}", headers=headers ) payments = payments_res.json() for p in payments: print(f"Payment {p['paymentId']}: {p['client']} paid {p['amount']} {p['currency']}") last_payment_id = p["paymentId"] # save this for next run