jobber
Jobber API integration with managed OAuth. Manage clients, jobs, invoices, quotes, properties, and team members for field service businesses. Use this skill when users want to create and manage service jobs, clients, quotes, invoices, or access scheduling data. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Requires network access and valid Maton API key.
Jobber
Access the Jobber API with managed OAuth authentication. Manage clients, jobs, invoices, quotes, properties, and team members for field service businesses.
Quick Start
# Get account information
python <<'EOF'
import urllib.request, os, json
query = '{"query": "{ account { id name } }"}'
req = urllib.request.Request('https://gateway.maton.ai/jobber/graphql', data=query.encode(), method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
API Type
Jobber uses a GraphQL API exclusively. All requests are POST requests to the /graphql endpoint with a JSON body containing the query field.
Base URL
https://gateway.maton.ai/jobber/graphql
The gateway proxies requests to api.getjobber.com/api/graphql and automatically injects your OAuth token and API version header.
Authentication
All requests require the Maton API key in the Authorization header:
Authorization: Bearer $MATON_API_KEY
The gateway automatically injects the X-JOBBER-GRAPHQL-VERSION header (currently 2025-04-16).
Environment Variable: Set your API key as MATON_API_KEY:
export MATON_API_KEY="YOUR_API_KEY"
Getting Your API Key
- Sign in or create an account at maton.ai
- Go to maton.ai/settings
- Copy your API key
Connection Management
Manage your Jobber OAuth connections at https://ctrl.maton.ai.
List Connections
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections?app=jobber&status=ACTIVE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Create Connection
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'app': 'jobber'}).encode()
req = urllib.request.Request('https://ctrl.maton.ai/connections', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Get Connection
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Response:
{
"connection": {
"connection_id": "cc61da85-8bf7-4fbc-896b-4e4eb9a5aafd",
"status": "ACTIVE",
"creation_time": "2026-02-07T09:29:19.946291Z",
"last_updated_time": "2026-02-07T09:30:59.990084Z",
"url": "https://connect.maton.ai/?session_token=...",
"app": "jobber",
"metadata": {}
}
}
Open the returned url in a browser to complete OAuth authorization.
Delete Connection
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}', method='DELETE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Specifying Connection
If you have multiple Jobber connections, specify which one to use with the Maton-Connection header:
python <<'EOF'
import urllib.request, os, json
query = '{"query": "{ account { id name } }"}'
req = urllib.request.Request('https://gateway.maton.ai/jobber/graphql', data=query.encode(), method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
req.add_header('Maton-Connection', 'cc61da85-8bf7-4fbc-896b-4e4eb9a5aafd')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
If omitted, the gateway uses the default (oldest) active connection.
API Reference
Account Operations
Get Account Information
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ account { id name } }"
}
Client Operations
List Clients
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ clients(first: 20) { nodes { id name emails { primary address } phones { primary number } } pageInfo { hasNextPage endCursor } } }"
}
Get Client by ID
POST /jobber/graphql
Content-Type: application/json
{
"query": "query($id: EncodedId!) { client(id: $id) { id name emails { primary address } phones { primary number } billingAddress { street city } } }",
"variables": { "id": "CLIENT_ID" }
}
Create Client
POST /jobber/graphql
Content-Type: application/json
{
"query": "mutation($input: ClientCreateInput!) { clientCreate(input: $input) { client { id name } userErrors { message path } } }",
"variables": {
"input": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "555-1234"
}
}
}
Update Client
POST /jobber/graphql
Content-Type: application/json
{
"query": "mutation($id: EncodedId!, $input: ClientUpdateInput!) { clientUpdate(clientId: $id, input: $input) { client { id name } userErrors { message path } } }",
"variables": {
"id": "CLIENT_ID",
"input": {
"email": "newemail@example.com"
}
}
}
Job Operations
List Jobs
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ jobs(first: 20) { nodes { id title jobNumber jobStatus client { name } } pageInfo { hasNextPage endCursor } } }"
}
Get Job by ID
POST /jobber/graphql
Content-Type: application/json
{
"query": "query($id: EncodedId!) { job(id: $id) { id title jobNumber jobStatus instructions client { name } property { address { street city } } } }",
"variables": { "id": "JOB_ID" }
}
Create Job
POST /jobber/graphql
Content-Type: application/json
{
"query": "mutation($input: JobCreateInput!) { jobCreate(input: $input) { job { id jobNumber title } userErrors { message path } } }",
"variables": {
"input": {
"clientId": "CLIENT_ID",
"title": "Lawn Maintenance",
"instructions": "Weekly lawn care service"
}
}
}
Invoice Operations
List Invoices
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ invoices(first: 20) { nodes { id invoiceNumber subject total invoiceStatus client { name } } pageInfo { hasNextPage endCursor } } }"
}
Get Invoice by ID
POST /jobber/graphql
Content-Type: application/json
{
"query": "query($id: EncodedId!) { invoice(id: $id) { id invoiceNumber subject total amountDue invoiceStatus lineItems { nodes { name quantity unitPrice } } } }",
"variables": { "id": "INVOICE_ID" }
}
Create Invoice
POST /jobber/graphql
Content-Type: application/json
{
"query": "mutation($input: InvoiceCreateInput!) { invoiceCreate(input: $input) { invoice { id invoiceNumber } userErrors { message path } } }",
"variables": {
"input": {
"clientId": "CLIENT_ID",
"subject": "Service Invoice",
"lineItems": [
{
"name": "Lawn Care",
"quantity": 1,
"unitPrice": 75.00
}
]
}
}
}
Quote Operations
List Quotes
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ quotes(first: 20) { nodes { id quoteNumber title quoteStatus client { name } } pageInfo { hasNextPage endCursor } } }"
}
Create Quote
POST /jobber/graphql
Content-Type: application/json
{
"query": "mutation($input: QuoteCreateInput!) { quoteCreate(input: $input) { quote { id quoteNumber } userErrors { message path } } }",
"variables": {
"input": {
"clientId": "CLIENT_ID",
"title": "Landscaping Quote",
"lineItems": [
{
"name": "Garden Design",
"quantity": 1,
"unitPrice": 500.00
}
]
}
}
}
Property Operations
List Properties
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ properties(first: 20) { nodes { id address { street city state postalCode } client { name } } pageInfo { hasNextPage endCursor } } }"
}
Request Operations
List Requests
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ requests(first: 20) { nodes { id title requestStatus client { name } } pageInfo { hasNextPage endCursor } } }"
}
User/Team Operations
List Users
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ users(first: 50) { nodes { id name { full } email { raw } } } }"
}
Custom Field Operations
List Custom Fields
POST /jobber/graphql
Content-Type: application/json
{
"query": "{ customFields(first: 50) { nodes { id name fieldType } } }"
}
Pagination
Jobber uses Relay-style cursor-based pagination:
# First page
POST /jobber/graphql
{
"query": "{ clients(first: 20) { nodes { id name } pageInfo { hasNextPage endCursor } } }"
}
# Next page using cursor
POST /jobber/graphql
{
"query": "{ clients(first: 20, after: \"CURSOR_VALUE\") { nodes { id name } pageInfo { hasNextPage endCursor } } }"
}
Response includes pageInfo:
{
"data": {
"clients": {
"nodes": [...],
"pageInfo": {
"hasNextPage": true,
"endCursor": "abc123"
}
}
}
}
Webhooks
Jobber supports webhooks for real-time event notifications:
CLIENT_CREATE- New client createdJOB_COMPLETE- Job marked completeQUOTE_CREATE- New quote createdQUOTE_APPROVAL- Quote approvedREQUEST_CREATE- New request createdINVOICE_CREATE- New invoice createdAPP_CONNECT- App connected
Webhooks include HMAC-SHA256 signatures for verification.
Code Examples
JavaScript
const query = `{ clients(first: 10) { nodes { id name emails { address } } } }`;
const response = await fetch('https://gateway.maton.ai/jobber/graphql', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MATON_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const data = await response.json();
Python
import os
import requests
query = '''
{
clients(first: 10) {
nodes { id name emails { address } }
}
}
'''
response = requests.post(
'https://gateway.maton.ai/jobber/graphql',
headers={
'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}',
'Content-Type': 'application/json'
},
json={'query': query}
)
data = response.json()
Notes
- Jobber uses GraphQL exclusively (no REST API)
- The gateway automatically injects the
X-JOBBER-GRAPHQL-VERSIONheader - Current gateway API version:
2025-04-16(latest) - Old API versions are supported for 12-18 months from release
- Use the GraphiQL explorer in Jobber's Developer Center for schema discovery
- IDs use
EncodedIdtype (base64 encoded) - pass as strings - Field naming: use
emails/phones(arrays),jobStatus/invoiceStatus/quoteStatus/requestStatus - Rate limits:
- DDoS protection: 2,500 requests per 5 minutes per app/account
- Query cost: Points-based using leaky bucket algorithm (max 10,000 points, restore 500/sec)
- Avoid deeply nested queries to reduce query cost
- IMPORTANT: When piping curl output to
jqor other commands, environment variables like$MATON_API_KEYmay not expand correctly in some shell environments
Error Handling
| Status | Meaning |
|---|---|
| 400 | Missing Jobber connection or malformed query |
| 401 | Invalid or missing Maton API key |
| 403 | Not authorized (check OAuth scopes) |
| 429 | Rate limited |
| 4xx/5xx | Passthrough error from Jobber API |
GraphQL errors appear in the response body:
{
"errors": [
{
"message": "Error description",
"locations": [...],
"path": [...]
}
]
}
Mutation errors appear in userErrors:
{
"data": {
"clientCreate": {
"client": null,
"userErrors": [
{
"message": "Email is required",
"path": ["input", "email"]
}
]
}
}
}
Troubleshooting: Invalid API Key
When you receive a "Invalid API key" error, ALWAYS follow these steps before concluding there is an issue:
- Check that the
MATON_API_KEYenvironment variable is set:
echo $MATON_API_KEY
- Verify the API key is valid by listing connections:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF