ProWorkflow API v4
Build powerful integrations with ProWorkflow's Nexus project management platform.
Getting Started
The API lives at:
https://api.proworkflow.com/api/v4
You'll need an API key or an OAuth access token to authenticate. See Authentication to choose between them.
How Responses Work
Every response comes back as JSON. You'll always get a status field that tells you if things worked ("success") or not ("error").
The actual data you requested lives in the data field. For lists of things, you'll also get a meta object with counts and pagination info:
// Getting a list of companies
{
"status": "success",
"data": [...], // Your companies
"meta": {
"count": 10, // How many in this response
"total": 250 // Total available
}
}
// Getting a single project
{
"status": "success",
"data": {
"id": 123,
"name": "Website Redesign"
}
}
// Something went wrong
{
"status": "error",
"data": [
"What went wrong"
]
}
Making Requests
For GET and DELETE requests, just add parameters to the URL.
For POST and PUT requests, send JSON in the body with Content-Type: application/json.
What You Can Do
The API gives you access to everything in ProWorkflow:
- Companies & Contacts
- Projects & Tasks
- Time Tracking & Expenses
- Invoices & Quotes
- Custom Fields & Tags
- Reports & Settings
Most endpoints let you:
- Filter results with query parameters
- Choose which fields to return
- Page through large datasets
- Sort by different fields
- Create multiple items at once
Check out the endpoint documentation for specifics on what each one can do.
Connect Claude — Want to use ProWorkflow from Claude? Follow Connect Claude: create an OAuth client, add the connector and sign in, with no code. The MCP Server reference lists every tool it can use.
Examples & Tools — See the Examples page for scripts, apps, and tools built on the API.
curl 'https://api.proworkflow.com/api/v4/companies?fields=name,code&pagesize=10' \ -H 'apikey: your-api-key'
curl -X POST 'https://api.proworkflow.com/api/v4/projects' \ -H 'Content-Type: application/json' \ -H 'apikey: your-api-key' \ -d '{"name": "New Project", "companyid": 123}'
Authentication
Every request needs credentials. Pick the method that matches what you're building:
| You want to | Use | Set up |
|---|---|---|
| Connect Claude to ProWorkflow | OAuth, through a Claude connector | Connect Claude |
| Build an app where people sign in with their own ProWorkflow login | OAuth 2.0 | OAuth 2.0 |
| Run a script, sync or other server-to-server integration | API key | API key below |
Setting up OAuth is self-serve: you don't need to contact Support.
How the two methods differ
| API key | OAuth access token | |
|---|---|---|
| Acts as | The account | The user who signed in |
| Permissions | None applied: full account access | The user's role, as in the ProWorkflow app |
| Sensitive fields | All visible | Hidden unless the user can view sensitive business info |
| Divisions | All | The user's division |
| Who can use it | Anyone holding the key | Staff users only. Client and contractor logins are rejected |
| Lifetime | Until the key is removed | 5 minutes, renewed with a refresh token |
| Header | apikey: <key> |
Authorization: Bearer <access_token> |
An API key bypasses every permission check. Keep it on a server, never in a browser or mobile app, and use OAuth when a person is behind the request.
OAuth (Bearer tokens)
OAuth lets an app act as a signed-in ProWorkflow user. A super user creates the account's OAuth client in Settings → Automation → OAuth Clients. Each account has one OAuth client.
- Connecting Claude? Follow Connect Claude. It takes a few minutes and needs no code.
- Building your own app? OAuth 2.0 covers the full authorization-code flow, token refresh and errors.
API key
Pass the key in the apikey header. Requests act at account level: no permission restrictions, all sensitive fields visible and all divisions accessible.
Format: xxxx-xxxx-xxxx-xxxx-PWFxxxx, optionally followed by a suffix (xxxx-xxxx-xxxx-xxxx-PWFxxxx-suffix).
If a request sends both an Authorization: Bearer header and an apikey header, the bearer token is used and the API key is ignored.
Try It Out
Use the Try it out panel in the sidebar to set your credentials, then use the Try It forms on any endpoint to make live requests.
curl 'https://api.proworkflow.com/api/v4/projects' \ -H 'apikey: xxxx-xxxx-xxxx-xxxx-PWFxxxx'
curl 'https://api.proworkflow.com/api/v4/projects' \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIs...'
Connect Claude
Connect Claude to ProWorkflow so it can find and update your projects, tasks, time, invoices, quotes and more from a chat. The connector works in Claude on the web, Claude Desktop and Claude mobile.
Each person signs in with their own ProWorkflow login, so Claude can only see and change what that person can in ProWorkflow.
You'll need:
- A ProWorkflow super user, to create the OAuth client. This is done once per account.
- The ProWorkflow MCP server URL:
https://api.proworkflow.com/api/v4/mcp - A ProWorkflow staff login for each person who connects. Client and contractor logins can't use the connector.
Step 1: Create the OAuth client in ProWorkflow
A super user does this once for the account.
- In ProWorkflow, go to Settings → Automation → OAuth Clients.
- Click Set up OAuth client.
- Under Redirect URIs, keep the prefilled
https://claude.ai/api/mcp/auth_callback. - Optionally add a Description, such as "Claude connector", then click Create client.
- In the OAuth client created dialog, copy the Client ID and the Client Secret, then click Done.
The Client Secret is shown only once. It's stored hashed and can't be retrieved later. If you lose it, rotate the secret (the rotate icon on the client) to get a new one.
Already have an OAuth client? An account can have only one. Click the pencil icon on it, add https://claude.ai/api/mcp/auth_callback to its Redirect URIs and save, then use its Client ID and Client Secret below.
Step 2: Add the connector in Claude
On a Free, Pro or Max plan, you add the connector to your own Claude account:
- Go to Customize → Connectors and click Add custom connector.
- Name it ProWorkflow and enter the server URL
https://api.proworkflow.com/api/v4/mcp. - Enter the Client ID and Client Secret from step 1. Depending on your dialog, they're under Advanced settings, or you choose Use your own OAuth client when it asks how to sign in.
- Click Add.
On a Team or Enterprise plan, an Owner adds the connector once for the organization:
- Go to Organization settings → Connectors, click Add, then Custom. If Claude asks for the connector type, choose Web.
- Enter the server URL, the Client ID and the Client Secret, then click Add.
Members then find ProWorkflow under Customize → Connectors, marked Custom.
Step 3: Connect and sign in
Each person connects with their own ProWorkflow login:
- In Customize → Connectors, click Connect on ProWorkflow.
- Sign in to ProWorkflow on the page that opens, with two-factor authentication if your account uses it.
- The first time, approve access. Claude then confirms that ProWorkflow is connected.
In a chat, click +, then Connectors, and make sure ProWorkflow is turned on. Try asking: "List my active projects" or "Which of my tasks are due this week?"
What Claude can do
Claude can search, read, create, update and delete projects, tasks, phases, time records, time allocations, invoices and quotes (with their line items), requests, messages, project notes, companies and contacts. It can upload and download files, and reach any other part of the API, such as settings. See the MCP Server reference for every tool.
Everything follows the signed-in person's ProWorkflow role: the same permissions, division and sensitive-field visibility as in the app. To stop Claude using a tool, such as the delete tools, select ProWorkflow under Customize → Connectors and set that tool to Blocked.
Staying connected
Claude refreshes your sign-in in the background. If you haven't used the connector for about two weeks, or it has been a year since you signed in, Claude asks you to connect again.
To stop Claude's access:
- For one person: they disconnect ProWorkflow in Customize → Connectors.
- For everyone: delete the OAuth client in ProWorkflow (the trash icon). Sign-ins stop working right away. A request already signed in can keep working for up to 5 minutes, until its access token expires.
If you rotate the Client Secret, Claude keeps using the old one, and it stops working. Claude can't change a connector's OAuth details after it's added, so remove the connector and add it again with the new secret. Everyone then connects again.
Claude Code
Claude Code signs in through a page on your own computer instead of claude.ai, so it needs its own redirect URI:
- Edit the OAuth client in ProWorkflow and add the redirect URI
http://localhost:8080/callback. Any free port works, as long as you use the same one in the next step. - Add the server with the command shown here. It prompts for the Client Secret.
- Run
/mcpin Claude Code and choose ProWorkflow to sign in.
Troubleshooting
| What you see | Cause | Fix |
|---|---|---|
| The ProWorkflow sign-in page shows an error instead of returning to Claude | The redirect URI on the OAuth client doesn't exactly match Claude's, or the Client ID is wrong | Check the client lists https://claude.ai/api/mcp/auth_callback exactly, and that the connector has the current Client ID |
| You sign in, then Claude says authorization failed | The Client Secret is wrong or was rotated | Remove the connector and add it again with the current secret |
| Claude can't connect after you sign in as a client or contractor | The connector is for staff users only | Sign in with a staff login |
| A tool fails with a permission error | Your ProWorkflow role doesn't allow that action | Ask a super user to change your role, or have someone with access do it |
https://api.proworkflow.com/api/v4/mcp
https://claude.ai/api/mcp/auth_callback
claude mcp add --transport http \ --client-id YOUR_CLIENT_ID --client-secret --callback-port 8080 \ proworkflow https://api.proworkflow.com/api/v4/mcp
OAuth 2.0
Use OAuth when a ProWorkflow user signs in to your app and the app calls the API as them. Their access token carries their permissions, division and sensitive-field visibility, exactly as in the ProWorkflow app.
Only staff users can use the API through OAuth. If a client or contractor signs in, the API rejects their token with 401.
Connecting Claude rather than building an app? Follow Connect Claude instead.
At a glance
| Setting | Value |
|---|---|
| Flow | Authorization code, with refresh tokens |
| Authorize URL | https://identity.proworkflow.com/auth |
| Token URL | https://identity.proworkflow.com/token |
| Discovery | https://identity.proworkflow.com/.well-known/openid-configuration |
| Client authentication | client_secret_post: client_id and client_secret in the form body, not a Basic header |
| Scopes | openid email. These are the only scopes. Request both |
| PKCE | S256 supported. Optional for your client, and recommended |
| Authorization code | Expires after 1 minute, single use |
| Access token | A JWT that lasts 5 minutes (expires_in: 300) |
| Refresh token | Lasts 14 days. Refreshing can keep a user signed in for up to a year |
| API header | Authorization: Bearer <access_token> |
1. Create an OAuth client
A super user creates the client in ProWorkflow. You don't need to contact Support.
- Go to Settings → Automation → OAuth Clients.
- Click Set up OAuth client.
- Under Redirect URIs, replace the prefilled Claude URI with your app's callback, such as
https://yourapp.com/oauth/callback. Click Add redirect URI for others, such ashttp://localhost:3000/oauth/callbackfor development. - Optionally add a Description, then click Create client.
- In the OAuth client created dialog, copy the Client ID and Client Secret, then click Done.
The Client Secret is shown only when the client is created or its secret is rotated. It's stored hashed and can't be retrieved later. If you lose it, rotate the secret.
Each account can have one OAuth client. Several apps, such as your own app and a Claude connector, can share it: add each app's callback to its redirect URIs.
2. Send the user to sign in
Redirect the user's browser to the authorize URL (example on the right):
| Parameter | Value |
|---|---|
client_id |
Your Client ID |
redirect_uri |
One of the client's redirect URIs, matched exactly: scheme, host, port, path and trailing slash |
response_type |
code |
scope |
openid email |
state |
A random value you store and check on the callback, to prevent CSRF |
code_challenge, code_challenge_method |
Optional PKCE: the base64url SHA-256 of a random verifier, and S256 |
The user signs in to ProWorkflow and, the first time, approves your app. The browser then returns to redirect_uri?code=...&state=.... Check that state matches the value you stored.
3. Exchange the code for tokens
Within 1 minute, POST the code to the token URL as application/x-www-form-urlencoded, with grant_type=authorization_code, the code, the same redirect_uri, your client_id and client_secret, and the code_verifier if you sent a PKCE challenge.
The response contains an access_token, which lasts 5 minutes, a refresh_token and an id_token.
4. Call the API
Send the access token on every request:
Authorization: Bearer <access_token>
5. Refresh the access token
Access tokens last 5 minutes. Refresh shortly before expires_in runs out, or when the API returns 401, by posting grant_type=refresh_token with the refresh token and your client credentials.
The response can include a new refresh_token. Always store the one you get back: once it's replaced, the old one stops working. A refresh token lasts 14 days. Refreshing in the second half of that period returns a new one with a fresh 14 days. An app that refreshes regularly keeps the user signed in for up to a year. After that, or after 14 days without a refresh, the user signs in again.
Manage your client
Your client is listed under Settings → Automation → OAuth Clients, with icons for each action:
- Edit (pencil): change its redirect URIs and description.
- Rotate secret: issue a new Client Secret. The old secret stops working immediately, so update every app that uses it.
- Delete (trash): sign-ins and token refreshes stop working immediately. Access tokens already issued keep working until they expire, within 5 minutes. A new client gets a new Client ID and Client Secret, so reconfigure your apps.
Common errors
| Error | Cause | Fix |
|---|---|---|
| The ProWorkflow sign-in page shows an error instead of returning to your app | The redirect_uri isn't registered on the client, or the client_id is wrong or deleted |
Register the exact URI, or use the current Client ID |
invalid_grant when exchanging a code |
The code expired (over 1 minute) or was already used, or redirect_uri differs from the authorize request |
Send the user through step 2 again |
invalid_grant when refreshing |
The refresh token expired, or was replaced by a newer one | Use the newest refresh token, or have the user sign in again |
invalid_client |
Wrong client ID or secret, the secret was sent in a Basic header, or the secret was rotated or the client deleted | Send client_id and client_secret in the form body, with the current secret |
401 from the API |
The access token expired, or the user isn't staff | Refresh and retry. Clients and contractors can't use OAuth |
403 from the API |
The user's role doesn't allow that action | Expected: OAuth follows the user's permissions |
Token URL errors are JSON in the standard OAuth form: {"error": "invalid_grant", "error_description": "..."}.
Security checklist
- Keep
client_secreton your server. Never ship it in a browser or mobile app. - Always check
state, and use PKCE. - Use HTTPS for every production redirect URI.
- Store refresh tokens encrypted. Rotate the client secret if it may have leaked.
https://identity.proworkflow.com/auth ?client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fyourapp.com%2Foauth%2Fcallback &response_type=code &scope=openid%20email &state=RANDOM_STRING
curl -X POST 'https://identity.proworkflow.com/token' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'code=AUTH_CODE' \ --data-urlencode 'redirect_uri=https://yourapp.com/oauth/callback' \ --data-urlencode 'client_id=YOUR_CLIENT_ID' \ --data-urlencode 'client_secret=YOUR_CLIENT_SECRET'
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 300,
"refresh_token": "a1b2c3d4...",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}
curl 'https://api.proworkflow.com/api/v4/companies?fields=name,code&pagesize=10' \ -H 'Authorization: Bearer ACCESS_TOKEN'
curl -X POST 'https://identity.proworkflow.com/token' \ --data-urlencode 'grant_type=refresh_token' \ --data-urlencode 'refresh_token=REFRESH_TOKEN' \ --data-urlencode 'client_id=YOUR_CLIENT_ID' \ --data-urlencode 'client_secret=YOUR_CLIENT_SECRET'
import express from "express"; import session from "express-session"; import crypto from "node:crypto"; const { PWF_CLIENT_ID, PWF_CLIENT_SECRET, SESSION_SECRET } = process.env; const REDIRECT_URI = "http://localhost:3000/oauth/callback"; const IDENTITY = "https://identity.proworkflow.com"; const app = express(); app.use(session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false })); // Step 2: send the user to sign in, with state and PKCE app.get("/login", (req, res) => { req.session.state = crypto.randomBytes(16).toString("hex"); req.session.verifier = crypto.randomBytes(32).toString("base64url"); const challenge = crypto.createHash("sha256") .update(req.session.verifier).digest("base64url"); const url = new URL(IDENTITY + "/auth"); url.search = new URLSearchParams({ client_id: PWF_CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: "code", scope: "openid email", state: req.session.state, code_challenge: challenge, code_challenge_method: "S256", }); res.redirect(url.toString()); }); // Step 3: check state, then exchange the code app.get("/oauth/callback", async (req, res) => { if (!req.query.code || req.query.state !== req.session.state) { return res.status(400).send("Sign-in failed"); } const r = await fetch(IDENTITY + "/token", { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: req.query.code, redirect_uri: REDIRECT_URI, code_verifier: req.session.verifier, client_id: PWF_CLIENT_ID, client_secret: PWF_CLIENT_SECRET, }), }); if (!r.ok) return res.status(400).send("Token exchange failed"); req.session.tokens = await r.json(); res.redirect("/"); }); // Step 4: call the API as the signed-in user. The access token lasts // 5 minutes: refresh it (step 5) when it expires or the API returns 401. app.get("/", async (req, res) => { if (!req.session.tokens) return res.redirect("/login"); const r = await fetch("https://api.proworkflow.com/api/v4/projects?pagesize=10", { headers: { Authorization: "Bearer " + req.session.tokens.access_token }, }); res.json(await r.json()); }); app.listen(3000);
Rate Limiting
The API enforces rate limiting to ensure fair usage and system stability.
Rate Limit
500 requests per 30-second window per API key, or per IP address for OAuth (bearer token) requests.
- API key requests are limited per API key
- OAuth requests are limited per IP address, so users of an app behind one IP share its limit
Response Headers
Every API response includes rate limit information in the headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window (500) |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Seconds until the window resets (max 30) |
Rate Limit Exceeded
When you exceed the rate limit, the API returns HTTP status 429 Too Many Requests. You must wait for the window to reset before making additional requests.
Best Practices
- Monitor the
X-RateLimit-Remainingheader to avoid hitting limits - Implement exponential backoff when you receive 429 responses
- Cache responses when possible to reduce API calls
- Use field selection to minimize payload sizes
- Batch operations where supported
Record Limits
The API will return a maximum of 30,000 records. If your request exceeds this limit then only the first 30,000 records will be returned. We strongly suggest you design your application and requests such that you will not come close to this limit. For optimal performance you should plan your requests to return no more than 1,000 records and typically no more than 500.
If your usage of the API is impacting on response times for other customers then we reserve the right to limit or remove your access to the API (we would, of course, contact you first to try to avoid taking that step).
You can limit the number of items returning and improve performance by using the paging options and we encourage you to use these options to deliver a better experience to your users.
HTTP/1.1 200 OK X-RateLimit-Limit: 500 X-RateLimit-Remaining: 485 X-RateLimit-Reset: 28
{
"status": "error",
"data": [
"Rate limit exceeded. Try again in 30 seconds."
]
}
Pagination
Most collection endpoints are paginated. Pass pagenumber and pagesize to get a specific page. By default the total record count is not calculated — pass includetotalrows=true to include meta.total in the response.
Pagination Parameters
| Parameter | Type | Description |
|---|---|---|
| pagenumber | integer | Page number to retrieve (starts at 1) |
| pagesize | integer | Number of items per page (max varies by endpoint) |
| includetotalrows | boolean | Set to true to run the count query and include meta.total in the response (default: false) |
Important: Both or Neither
You must provide both pagenumber and pagesize together, or neither. Providing only one will result in a validation error.
Default Behavior
- Without pagination parameters: Returns all matching records (up to endpoint limit)
- With pagination parameters: Returns specified page with metadata
Response Metadata
Paginated responses include a meta object with pagination details:
{
"status": "success",
"data": [...],
"meta": {
"count": 10, // Items in this response
"total": 250, // Total matching records
"page": 1, // Current page number
"pagesize": 10 // Items per page
}
}
Total Count
The total field in the meta object reflects the true total count of all records matching your query filters, not just the items in the current page. This allows you to:
- Calculate the total number of pages:
Math.ceil(total / pagesize) - Show accurate progress indicators
- Know when you've retrieved all records
Opting in with includetotalrows
By default, the count query is not executed and total is omitted from the response meta. To include it, pass includetotalrows=true:
GET /api/v4/projects/items?pagenumber=1&pagesize=10&includetotalrows=true
{
"status": "success",
"data": [...],
"meta": {
"count": 10,
"total": 13875,
"page": 1,
"pagesize": 10
}
}
When includetotalrows is omitted or false, the total field is not returned and the count query is skipped, which can significantly improve response times on large datasets:
{
"status": "success",
"data": [...],
"meta": {
"count": 10,
"page": 1,
"pagesize": 10
}
}
Performance Tip
Only set includetotalrows=true when you need the total (e.g. to display a page count). Skipping the count query can make paginated requests noticeably faster on endpoints with many matching records.
Sorting
Control the order of results with sorting parameters:
| Parameter | Description |
|---|---|
| sortby | Field to sort by (varies by endpoint) |
| sortorder | asc (ascending) or desc (descending) |
Common sort fields:
id- Record IDname- Name fieldcode- Code/identifiercreatedon- Creation datelastmodified- Last modification datesortorder- Custom drag-and-drop position (see below)
Custom Ordering
Some entities (projects, items, invoices, quotes) support drag-and-drop ordering via a dedicated endpoint. Use sortby=sortorder to return results in their custom order, and use the /order sub-endpoint to change position:
PUT /api/v4/{resource}/:id/order
Provide exactly one placement field in the request body:
| Field | Description |
|---|---|
| placeafterid | ID of the record this item should be positioned after |
| placebeforeid | ID of the record this item should be positioned before |
Endpoints supporting custom ordering:
PUT /api/v4/projects/:projectid/orderPUT /api/v4/projects/items/:itemid/orderPUT /api/v4/quotes/items/:itemid/orderPUT /api/v4/invoices/items/:itemid/orderPUT /api/v4/invoices/:invoiceid/orderPUT /api/v4/quotes/:quoteid/order
# Move item 42 to appear after item 41 curl -X PUT 'https://api.proworkflow.com/api/v4/projects/items/42/order' \ -H 'apikey: your-api-key' \ -H 'Content-Type: application/json' \ -d '{"placeafterid": 41}'
{
"status": "success",
"message": "Project item reordered"
}
curl 'https://api.proworkflow.com/api/v4/companies?pagenumber=1&pagesize=50&sortby=name&sortorder=asc' \ -H 'apikey: your-api-key'
{
"status": "success",
"data": [
{"id": 1, "name": "Acme Corp"},
{"id": 2, "name": "Beta Inc"}
],
"meta": {
"count": 2,
"page": 1,
"pagesize": 50
}
}
curl 'https://api.proworkflow.com/api/v4/companies?pagenumber=1&pagesize=50&includetotalrows=true' \ -H 'apikey: your-api-key'
{
"status": "success",
"data": [
{"id": 1, "name": "Acme Corp"},
{"id": 2, "name": "Beta Inc"}
],
"meta": {
"count": 2,
"total": 150,
"page": 1,
"pagesize": 50
}
}
async function getAllCompanies() { let page = 1; const pageSize = 100; const allCompanies = []; while (true) { // Request total count on the first page only const withTotal = page === 1 ? '&includetotalrows=true' : ''; const response = await fetch( `/api/v4/companies?pagenumber=${page}&pagesize=${pageSize}${withTotal}` ); const result = await response.json(); allCompanies.push(...result.data); // Stop when we've retrieved all records if (allCompanies.length >= result.meta.total || result.data.length < pageSize) { break; } page++; } return allCompanies; }
Query Parameters Reference
A quick reference for the filtering and field selection parameters supported across GET endpoints. Parameters combine with AND logic unless noted.
Field Selection
Control which fields are returned in the response to optimize payload size.
| Parameter | Example | Description |
|---|---|---|
| fields | name,code,type | Comma-separated list of fields to return |
| fields | all | Return all available fields (subject to permissions) |
Notes:
- The
idfield is always included automatically - Invalid field names are silently ignored
- Field names are case-insensitive
- Default fields vary by endpoint when not specified
Search & Text Filtering
Search within text fields using these parameters:
| Parameter | Example | Description |
|---|---|---|
| search | acme | General search across default fields |
| search | !test | Negation search (exclude matches) |
| searchname | corp | Search in name field only |
| searchcode | ABC | Search in code field only |
| searchemail | @example.com | Search in email field only |
Negation Search:
- Prefix search term with
!to exclude matches - Example:
?search=!testreturns items NOT containing "test" - Works with field-specific searches too:
?searchname=!admin
Type & Status Filters
Filter by enumerated values like type, status, or state:
| Parameter | Example | Description |
|---|---|---|
| type | client | Filter by single type |
| type | client,contractor | Multiple types (OR logic) |
| status | active | Filter by status |
| status | active,pending | Multiple statuses (OR logic) |
Common Values:
- Company types:
client,contractor,staff,other - Project status:
active,complete,cancelled,archived - Task status:
pending,inprogress,complete
Date & Time Filters
Filter by dates using absolute or relative time:
| Parameter | Example | Description |
|---|---|---|
| lastmodifiedfrom | 2024-01-18T13:20 | ISO8601 date/time |
| lastmodifiedfrom | 12h | Last 12 hours |
| lastmodifiedfrom | 5d | Last 5 days |
| lastmodifiedfrom | 2w | Last 2 weeks |
| lastmodifiedfrom | 1m | Last 1 month |
| lastmodifiedto | 2024-12-31 | Modified before date |
| createdfrom | 2024-01-01 | Created after date |
| createdto | 2024-12-31 | Created before date |
Relative Time Units:
n- minutes (e.g.,30n= 30 minutes)h- hours (e.g.,24h= 24 hours)d- days (e.g.,7d= 7 days)w- weeks (e.g.,4w= 4 weeks)m- months (e.g.,3m= 3 months)
UTC Variants:
Some endpoints support UTC versions: lastmodifiedutcfrom, lastmodifiedutcto
ID Filters
Filter by specific IDs or ID ranges:
| Parameter | Example | Description |
|---|---|---|
| id | 1,2,3,4 | Specific IDs (comma-separated) |
| idfrom | 100 | IDs greater than or equal to value |
| idto | 200 | IDs less than or equal to value |
Relationship Filters
Filter by related entities:
| Parameter | Example | Description |
|---|---|---|
| companyid | 123 | Filter by company |
| projectid | 456 | Filter by project |
| contactid | 789 | Filter by contact |
| tagid | 1,2,3 | Has ANY of these tags (OR) |
| divisionid | 10 | Filter by division (Advanced plans) |
Combining Parameters
This query finds:
- Client projects
- That are active OR pending
- Modified in the last 7 days
- NOT containing "test" in searchable fields
- Tagged with tag ID 1, 2, OR 3
- Returns only specified fields
- First 25 results
- Sorted by last modified date, newest first
curl 'https://api.proworkflow.com/api/v4/projects?' \ 'type=client&' \ 'status=active,pending&' \ 'lastmodifiedfrom=7d&' \ 'search=!test&' \ 'tagid=1,2,3&' \ 'fields=name,code,status,budget&' \ 'pagenumber=1&pagesize=25&' \ 'sortby=lastmodified&sortorder=desc' \ -H 'apikey: your-api-key'
Error Handling
All error responses have "status": "error" and a data array with one or more messages describing what went wrong.
HTTP Status Codes
| Code | Meaning | When Used |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE (no response body) |
| 400 | Bad Request | Validation failed, invalid parameters |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but insufficient permissions |
| 404 | Not Found | Resource doesn't exist or no access |
| 409 | Conflict | Resource conflict (e.g., duplicate) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
| 503 | Service Unavailable | Temporary maintenance/overload |
Error Response Structure
All error responses follow this format:
{
"status": "error",
"data": [
"Error message 1",
"Error message 2"
]
}
The data array contains one or more descriptive error messages explaining what went wrong.
{
"status": "error",
"data": [
"Name is required",
"Invalid value for parameter 'type': 'invalid'. Allowed values: client, contractor, staff, other",
"Invalid date format for 'lastmodifiedfrom'. Use ISO8601 or relative time (12h, 5d)",
"Both 'pagenumber' and 'pagesize' must be provided together"
]
}
{
"status": "error",
"data": [
"You do not have permission to view invoices"
]
}
{
"status": "error",
"data": [
"Authentication required"
]
}
{
"status": "error",
"data": [
"Company not found"
]
}
async function makeApiRequest(url, options) { try { const response = await fetch(url, options); const data = await response.json(); // Check for error status in response if (data.status === "error") { throw new Error(data.data.join(", ")); } // Handle specific HTTP status codes if (!response.ok) { switch (response.status) { case 401: // Redirect to login break; case 429: // Implement retry with backoff break; case 500: // Log error and show user message break; } } return data; } catch (error) { console.error("API Error:", error); throw error; } }
Permissions & Access Control
Access control works differently depending on how you authenticate. API keys bypass all permission checks. JWT tokens enforce the full user permission model.
Authentication Impact on Permissions
Critical Difference
API Key Authentication: Bypasses ALL permission checks - full account access JWT Authentication: Enforces user-specific permissions and restrictions
User Types
Every user has a type that determines their general access level:
| Type ID | Type | Description |
|---|---|---|
| 1 | Client | External client users with limited access |
| 2 | Contractor | External contractors with project access |
| 3 | Staff | Internal staff with full system access |
| 4 | Other | Custom user type with configurable access |
Account Plans
Account plans determine available features:
| Plan ID | Plan | Features |
|---|---|---|
| 1 | Basic | Standard features, single division |
| 3 | Advanced | Teams, groups, multiple divisions |
Permission System
Permissions are identified by numeric IDs and control specific actions:
| Permission ID | Name | Controls |
|---|---|---|
| 2 | ViewAllWork | See all work across the account |
| 3 | ViewItem | View tasks and items |
| 4 | ViewProject | View projects |
| 7 | ViewTimelog | View time entries |
| 8 | ViewContactNote | View contact/company notes |
| 11 | ViewSensitiveBusinessInfo | View financial/budget fields |
| 12 | ViewInvoice | View invoices |
| 13 | ViewQuote | View quotes |
| 14 | ViewCustomfields | View custom fields |
| 18 | ViewUserPermissions | View user permissions |
| 23 | ViewTeamWork | See team members' work |
| 24 | ViewAllWork | Division-scoped view all |
Permission Actions
Each permission supports different actions:
view- Read access to resourcesadd- Create new resourcesedit- Update existing resourcesdelete- Delete resources
Sensitive Field Filtering
Users without ViewSensitiveBusinessInfo (Permission 11) have financial fields automatically hidden:
Affected Resources & Fields:
| Resource | Hidden Fields |
|---|---|
| Project | budget, estBillAmount, invoicedAmount, paidAmount, estProfit, costs |
| Invoice | estBillAmount, estBillAmountTax, paid, budget, amounts |
| ProjectItem | budget, billAmount, estBillAmount, labour prices |
| Contact | payrate, payratemultiplier, defaulthourlyrateid |
| Company | Financial summaries, credit information |
API Key Exception
When using API key authentication, ALL sensitive fields are visible regardless of permission settings, as API keys represent account-level access.
Division & Team Filtering
For Advanced plan accounts with multiple divisions:
Division Access:
- Users are assigned to a specific division
- Can only see/modify resources in their division
divisionidparameter filters results- Cross-division access requires special permissions
Team Access:
- Users can be part of teams within divisions
ViewTeamWork(Permission 23) allows seeing team members' work- Without team permission, users see only their own assigned work
Contact Filter Modes
The API uses contact filter modes to control work visibility:
| Mode | Name | Visibility |
|---|---|---|
| 0 | ViewAllWork | See all work (requires permission) |
| 1 | Default | See only own assigned work |
| 2 | ViewTeamWork | See team members' work |
{
"status": "error",
"data": [
"You do not have permission to view invoices"
]
}
// User without ViewSensitiveBusinessInfo sees: { "status": "success", "data": { "id": 123, "name": "Project Alpha", "status": "active" // budget field omitted // estBillAmount field omitted } }
// User in division 5 only sees their division's data curl 'https://api.proworkflow.com/api/v4/projects' \ -H 'Authorization: Bearer <jwt-token>' // Returns only projects in user's division
Field Reference
The API provides flexible field selection to optimize response payloads. Use the fields parameter to control which fields are returned.
Field Selection
| Parameter Value | Behavior |
|---|---|
fields=name,code,type | Returns only specified fields (plus id) |
fields=all | Returns all available fields |
| omitted | Returns default fields for endpoint |
Important Notes:
- The
idfield is always included automatically - Field names are case-insensitive
- Unknown field names are silently ignored
- Some fields require specific permissions to view
Common Field Types
| Type | Format | Example | Notes |
|---|---|---|---|
| ID | int64 | 12345 | Unique identifier |
| String | UTF-8 text | "Acme Corp" | Text fields |
| Date | ISO 8601 | "2024-06-15" | Date only |
| DateTime | ISO 8601 | "2024-06-15T14:30:00" | Date and time |
| Decimal | float64 | 1250.50 | Money, percentages |
| Hours | Decimal | 1.5 | 1.5 = 1h 30m |
| Boolean | bool | true | True/false flags |
| Binary | Hex string | "0xABCDEF" | Sort order field |
| Array | JSON array | [{...}] | Related objects |
Sensitive Financial Fields
Certain financial fields are restricted based on permissions. Users without ViewSensitiveBusinessInfo permission will not see these fields:
Permission-Restricted Fields
The following fields are hidden from users without financial permissions (except when using API key authentication):
| Resource | Sensitive Fields |
|---|---|
| Project | budget, estBillAmount, estBillAmountTax, estInTotal, estInTotalTax, estProfit, estProfitPercentage, invoicedAmount, invoicedAmountTax, paidAmount, paidAmountTax, actualCost, actualRevenue, actualProfit |
| ProjectItem | budget, billAmount, estBillAmount, estInTotal, inLabourPrice, outLabourPrice, costPerHour, estCost, estRevenue, estProfit |
| Invoice | estBillAmount, estBillAmountTax, estIn, estInTax, paid, paidTax, budget, amounts, tax rates |
| Quote | total, totalTax, budget, itemized costs |
| Contact | payrate, payratemultiplier, defaulthourlyrateid, costPerHour, chargeoutrate |
| Timelog | cost, charge, profit, hourlyRate |
| Expense | cost, markup, total |
JSON Array Fields
Many resources include arrays of related objects:
| Field | Structure | Available On |
|---|---|---|
tags | [{id, name, color}] | Companies, Contacts, Projects, Items, Invoices, Quotes |
contacts | [{id, firstname, lastname, email}] | Companies, Projects |
staff | [{id, firstname, lastname, email}] | Projects, Items |
assignedto | [{id, firstname, lastname}] | Tasks, Items |
customfields | [{id, name, value, type}] | All major entities |
attachments | [{id, filename, size, url}] | Projects, Items, Invoices |
roles | [{id, name, color}] | Contacts |
Status Values by Entity
The status field represents different states for each entity type:
| Entity | Valid Status Values |
|---|---|
| Company | active, inactive, archived |
| Contact | active, inactive, archived |
| Project | pending, active, complete, cancelled, archived |
| Task/Item | pending, inprogress, complete, cancelled |
| Invoice | draft, submitted, authorised, paid, voided |
| Quote | pending, sent, accepted, declined, expired |
Computed vs Stored Fields
Stored Fields - Directly from database, support filtering/sorting:
id,name,code,status,typecreatedon,lastmodifiedstartdate,duedate
Computed Fields - Calculated at runtime, may not support filtering:
percentcomplete- Calculated from task completiontimeremaining- Based on estimates vs actualsprofitmargin- Calculated from costs/revenuefullname- Concatenated from firstname + lastnameage- Calculated from dates
Examples
Best Practices
- Request only needed fields - Reduces payload size and improves performance
- Check for field existence - Fields may be omitted based on permissions
- Handle null values - Many fields can be null when not set
- Parse arrays properly - Array fields may be empty arrays
[]or omitted entirely - Respect field types - Don't assume string when API returns numbers
GET /api/v4/companies
# Returns: id, code, name, type (defaults)
GET /api/v4/projects?fields=name,status,budget,tags # Returns: id, name, status, budget, tags # Note: budget may be hidden if user lacks permission
GET /api/v4/contacts?fields=all # Returns all available fields for contacts # Sensitive fields still subject to permissions
{
"id": 42,
"name": "Website Redesign",
"status": "active",
"tags": [
{"id": 1, "name": "Urgent", "color": "FF5733"},
{"id": 5, "name": "Frontend", "color": "2196F3"}
],
"staff": [
{"id": 10, "firstname": "John", "lastname": "Doe"},
{"id": 15, "firstname": "Jane", "lastname": "Smith"}
]
}
Batch Operations
Several endpoints change many records in one request. They take the IDs of the records in the body.
Bulk updates
PUT on a collection applies the same changes to every record in ids. Only the fields you send change.
| Endpoint | Changes |
|---|---|
PUT /projects |
Projects |
PUT /projects/items |
Project items (tasks) |
PUT /projects/items/workstage |
Project items' workstage |
PUT /projects/phases |
Project phases' budget and billing |
PUT /invoices/items, PUT /invoices/phases |
Invoice line items and phases |
PUT /quotes, PUT /quotes/items, PUT /quotes/phases |
Quotes, quote line items and phases |
Bulk date shifts
PUT /projects/adjustdates moves projects' start and due dates by shiftdates days (negative moves them earlier), and their items' dates by shiftchilddates. PUT /projects/phases/adjustdates, /invoices/phases/adjustdates and /quotes/phases/adjustdates do the same for phases.
Bulk deletes
DELETE on a collection deletes every record in ids: DELETE /projects, /invoices, /quotes, /timeallocations and /files.
Many records at once
These add or remove a relationship on several records: id lists the records and the other array the values.
| Endpoint | Does |
|---|---|
POST / DELETE /projects/items/contacts |
Assign or unassign contacts on items |
POST / DELETE /projects/items/tags |
Add or remove item tags |
POST / DELETE /projects/items/prerequisites |
Add or remove item prerequisites |
PUT /projects/items/complete |
Complete items |
PUT /projects/items/move |
Move items to another phase |
POST / DELETE /messages/contacts |
Add or remove contacts on messages |
PUT /time/contact, PUT /time/rate |
Reassign time records, or change their rate |
Creating several records
Create endpoints take one record per request: send an array and they answer 400 Invalid request body. Send one request per record.
Time allocations are the exception. POST /timeallocations takes an array and creates them all, and PUT /timeallocations updates several, each with its id.
All deletes are soft deletes: records are marked deleted and stay in the database.
curl -X PUT 'https://api.proworkflow.com/api/v4/projects' \ -H 'Content-Type: application/json' \ -H 'apikey: your-api-key' \ -d '{"ids": [101, 102, 103], "priorityid": 2}'
curl -X DELETE 'https://api.proworkflow.com/api/v4/quotes' \ -H 'Content-Type: application/json' \ -H 'apikey: your-api-key' \ -d '{"ids": [55, 56]}'
curl -X POST 'https://api.proworkflow.com/api/v4/projects/items/contacts' \ -H 'Content-Type: application/json' \ -H 'apikey: your-api-key' \ -d '{"id": [701, 702, 703], "contactid": [12, 15]}'
curl -X POST 'https://api.proworkflow.com/api/v4/timeallocations' \ -H 'Content-Type: application/json' \ -H 'apikey: your-api-key' \ -d '[{"itemid": 701, "contactid": 12, "timeallocated": 120}, {"itemid": 702, "contactid": 12, "timeallocated": 60}]'
{
"status": "success",
"message": "Projects updated"
}
Data Types & Models
Field types and formats used across the API.
Nullable Fields
Many fields can be null, indicated by omission from the response when using omitempty:
{
"id": 1,
"name": "Project Alpha"
// budget field omitted if null
}
Common Nullable Fields:
- Optional text fields (description, notes)
- Foreign keys (companyid, projectid when not required)
- Calculated fields (totals, percentages)
- Date fields (completeddate, duedate)
Date & Time Handling
Date Formats:
- ISO8601:
2024-01-18T13:20:00(local time) - ISO8601 with timezone:
2024-01-18T13:20:00Z(UTC) - Date only:
2024-01-18
Relative Time (for filters):
12h- 12 hours ago5d- 5 days ago2w- 2 weeks ago1m- 1 month ago
Timezone Handling
Dates without timezone indicators are interpreted as account local time. Use UTC variants of parameters (e.g., lastmodifiedutcfrom) for timezone-agnostic filtering.
Enumerated Types
Many fields accept only specific values:
Company/Contact Types:
"type": "client" | "contractor" | "staff" | "other"
Project Status:
"status": "active" | "complete" | "cancelled" | "archived" | "pending"
Task Status:
"status": "pending" | "inprogress" | "complete" | "cancelled"
Priority Levels:
"priority": 1 | 2 | 3 | 4 | 5 // 1=Highest, 5=Lowest
Billing Types:
"billtype": 0 | 1 | 2 | 3
// 0=Non-billable, 1=Hourly, 2=Fixed, 3=Recurring
Money & Financial Fields
Financial values are represented as decimals with up to 2 decimal places:
{
"budget": 50000.00,
"spent": 12500.50,
"remaining": 37499.50,
"hourlyrate": 150.00
}
Important: Some financial fields may be hidden based on user permissions. See Permissions documentation.
Special Field Types
Color Fields:
"color": "4A90E2" // 6-character hex color without #
Email Fields:
"email": "[email protected]" // Validated email format
URL Fields:
"website": "https://example.com" // Valid URL format
Composite Objects
Address Object:
"address": {
"line1": "123 Main St",
"line2": "Suite 100",
"city": "New York",
"state": "NY",
"postalcode": "10001",
"country": "USA"
}
Contact Name Object:
"contact": {
"id": 123,
"firstname": "John",
"lastname": "Doe",
"fullname": "John Doe",
"email": "[email protected]"
}
Field Omission Rules
Fields are omitted from responses when:
- They are null and marked with
omitempty - User lacks permission to view them
- They weren't requested via
fieldsparameter - They are empty arrays or objects
// API returns string IDs in some cases const companyId = parseInt(response.companyid, 10);
// Parse ISO8601 date const createdDate = new Date(response.createdon); // Format for API input const isoDate = createdDate.toISOString().split('T')[0];
// Safely access nullable fields const budget = response.budget ?? 0; const description = response.description ?? ""; // Check if field exists if ('budget' in response) { // Field was included but might be null }
Search & Filtering
This page covers the search and date range options. For the full parameter reference see Query Parameters.
Text Search
Search within text fields using various search parameters:
General Search
GET /api/v4/companies?search=acme
Searches across default searchable fields (usually name, code, description).
Field-Specific Search
GET /api/v4/contacts?searchname=john
GET /api/v4/[email protected]
GET /api/v4/companies?searchcode=ABC
Search within specific fields for more targeted results.
Negation Search
Prefix any search with ! to exclude matches:
GET /api/v4/projects?search=!test
Returns all projects that DON'T contain "test".
Search Behavior
- Searches are case-insensitive
- Partial matches are supported (SQL LIKE %term%)
- Multiple search parameters combine with AND
- Special characters should be URL-encoded
Filter Combinations
Filters work together to progressively narrow results:
Tag Filtering
Filter items by tags:
# Has ANY of these tags (OR logic)
GET /api/v4/projects?tagid=1,2,3
# Returns projects tagged with tag 1 OR 2 OR 3
Note: Currently no support for "has ALL tags" filtering.
Date Range Filtering
Filter by date ranges using absolute or relative dates:
Absolute Dates
GET /api/v4/projects?createdfrom=2024-01-01&createdto=2024-12-31
GET /api/v4/items?duefrom=2024-06-01&dueto=2024-06-30
Relative Time
# Items modified in last 24 hours
GET /api/v4/items?lastmodifiedfrom=24h
# Items created in last week
GET /api/v4/items?createdfrom=1w
# Items due in next 30 days
GET /api/v4/items?dueto=30d
Relative Time Units:
n- minutes (30n = 30 minutes)h- hours (24h = 24 hours)d- days (7d = 7 days)w- weeks (2w = 2 weeks)m- months (3m = 3 months)
Nested Resource Filtering
Access filtered subsets via nested endpoints:
# Get contacts for a specific company
GET /api/v4/companies/123/contacts
# Get active projects for a company
GET /api/v4/companies/123/projects?status=active
# Get items assigned to a contact
GET /api/v4/contacts/456/items
Performance Tips
- Index-friendly filters (fast):
id,status,type,companyid,projectid - Full-text search (slower):
search,searchname,searchdescription - Combine with sorting: Add
sortbyandsortorderfor consistent ordering - Use specific searches:
searchcode=ABCis faster thansearch=ABC
curl 'https://api.proworkflow.com/api/v4/projects?' \ 'type=client&' \ # Client projects 'status=active&' \ # That are active 'search=website&' \ # Containing "website" 'tagid=5&' \ # Tagged with ID 5 'lastmodifiedfrom=7d' \ # Modified in last 7 days -H 'apikey: your-api-key'
GET /api/v4/items? status=pending,inprogress& dueto=0d& sortby=duedate& sortorder=asc # Tasks that are not complete # With due date before today # Sorted by due date, oldest first
GET /api/v4/projects? type=client& lastmodifiedfrom=7d& fields=name,status,lastmodified,client& pagenumber=1& pagesize=20 # Client projects only # Modified in last 7 days # Select specific fields # First 20 results
GET /api/v4/invoices? status=submitted,authorised& search=!paid& sortby=total& sortorder=desc # Not yet paid invoices # Excluding those with "paid" in description # Sorted by total, highest first