ProWorkflow ProWorkflow

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.

Get Companies
cURL
curl 'https://api.proworkflow.com/api/v4/companies?fields=name,code&pagesize=10' \
  -H 'apikey: your-api-key'
Create Project
cURL
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.

API key
cURL
curl 'https://api.proworkflow.com/api/v4/projects' \
  -H 'apikey: xxxx-xxxx-xxxx-xxxx-PWFxxxx'
OAuth access token
cURL
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.

  1. In ProWorkflow, go to Settings → Automation → OAuth Clients.
  2. Click Set up OAuth client.
  3. Under Redirect URIs, keep the prefilled https://claude.ai/api/mcp/auth_callback.
  4. Optionally add a Description, such as "Claude connector", then click Create client.
  5. 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:

  1. Go to Customize → Connectors and click Add custom connector.
  2. Name it ProWorkflow and enter the server URL https://api.proworkflow.com/api/v4/mcp.
  3. 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.
  4. Click Add.

On a Team or Enterprise plan, an Owner adds the connector once for the organization:

  1. Go to Organization settings → Connectors, click Add, then Custom. If Claude asks for the connector type, choose Web.
  2. 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:

  1. In Customize → Connectors, click Connect on ProWorkflow.
  2. Sign in to ProWorkflow on the page that opens, with two-factor authentication if your account uses it.
  3. 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:

  1. 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.
  2. Add the server with the command shown here. It prompts for the Client Secret.
  3. Run /mcp in 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
MCP server URL
URL
https://api.proworkflow.com/api/v4/mcp
Redirect URI for Claude
URL
https://claude.ai/api/mcp/auth_callback
Add to Claude Code
Shell
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.

  1. Go to Settings → Automation → OAuth Clients.
  2. Click Set up OAuth client.
  3. 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 as http://localhost:3000/oauth/callback for development.
  4. Optionally add a Description, then click Create client.
  5. 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_secret on 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.
Step 2: Authorize URL
URL
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
Step 3: Exchange the code
cURL
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'
Token response
JSON
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 300,
  "refresh_token": "a1b2c3d4...",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}
Step 4: Call the API
cURL
curl 'https://api.proworkflow.com/api/v4/companies?fields=name,code&pagesize=10' \
  -H 'Authorization: Bearer ACCESS_TOKEN'
Step 5: Refresh the access token
cURL
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'
Node.js (Express)
JavaScript
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:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window (500)
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetSeconds 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-Remaining header 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.

Rate Limit Headers
Response Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 485
X-RateLimit-Reset: 28
429 Response
429 Too Many Requests
{
  "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

ParameterTypeDescription
pagenumberintegerPage number to retrieve (starts at 1)
pagesizeintegerNumber of items per page (max varies by endpoint)
includetotalrowsbooleanSet 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:

ParameterDescription
sortbyField to sort by (varies by endpoint)
sortorderasc (ascending) or desc (descending)

Common sort fields:

  • id - Record ID
  • name - Name field
  • code - Code/identifier
  • createdon - Creation date
  • lastmodified - Last modification date
  • sortorder - 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:

FieldDescription
placeafteridID of the record this item should be positioned after
placebeforeidID of the record this item should be positioned before

Endpoints supporting custom ordering:

  • PUT /api/v4/projects/:projectid/order
  • PUT /api/v4/projects/items/:itemid/order
  • PUT /api/v4/quotes/items/:itemid/order
  • PUT /api/v4/invoices/items/:itemid/order
  • PUT /api/v4/invoices/:invoiceid/order
  • PUT /api/v4/quotes/:quoteid/order
Reorder a Project Item
cURL
# 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}'
Reorder Response
200 OK
{
  "status": "success",
  "message": "Project item reordered"
}
First Page
cURL
curl 'https://api.proworkflow.com/api/v4/companies?pagenumber=1&pagesize=50&sortby=name&sortorder=asc' \
  -H 'apikey: your-api-key'
Paginated Response
200 OK
{
  "status": "success",
  "data": [
    {"id": 1, "name": "Acme Corp"},
    {"id": 2, "name": "Beta Inc"}
  ],
  "meta": {
    "count": 2,
    "page": 1,
    "pagesize": 50
  }
}
First Page with Total Count
cURL
curl 'https://api.proworkflow.com/api/v4/companies?pagenumber=1&pagesize=50&includetotalrows=true' \
  -H 'apikey: your-api-key'
Paginated Response with Total
200 OK
{
  "status": "success",
  "data": [
    {"id": 1, "name": "Acme Corp"},
    {"id": 2, "name": "Beta Inc"}
  ],
  "meta": {
    "count": 2,
    "total": 150,
    "page": 1,
    "pagesize": 50
  }
}
Iterate All Pages
JavaScript
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.

ParameterExampleDescription
fieldsname,code,typeComma-separated list of fields to return
fieldsallReturn all available fields (subject to permissions)

Notes:

  • The id field 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:

ParameterExampleDescription
searchacmeGeneral search across default fields
search!testNegation search (exclude matches)
searchnamecorpSearch in name field only
searchcodeABCSearch in code field only
searchemail@example.comSearch in email field only

Negation Search:

  • Prefix search term with ! to exclude matches
  • Example: ?search=!test returns items NOT containing "test"
  • Works with field-specific searches too: ?searchname=!admin

Type & Status Filters

Filter by enumerated values like type, status, or state:

ParameterExampleDescription
typeclientFilter by single type
typeclient,contractorMultiple types (OR logic)
statusactiveFilter by status
statusactive,pendingMultiple 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:

ParameterExampleDescription
lastmodifiedfrom2024-01-18T13:20ISO8601 date/time
lastmodifiedfrom12hLast 12 hours
lastmodifiedfrom5dLast 5 days
lastmodifiedfrom2wLast 2 weeks
lastmodifiedfrom1mLast 1 month
lastmodifiedto2024-12-31Modified before date
createdfrom2024-01-01Created after date
createdto2024-12-31Created 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:

ParameterExampleDescription
id1,2,3,4Specific IDs (comma-separated)
idfrom100IDs greater than or equal to value
idto200IDs less than or equal to value

Relationship Filters

Filter by related entities:

ParameterExampleDescription
companyid123Filter by company
projectid456Filter by project
contactid789Filter by contact
tagid1,2,3Has ANY of these tags (OR)
divisionid10Filter 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
Complex Query
cURL
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

CodeMeaningWhen Used
200OKSuccessful GET, PUT, PATCH, DELETE
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE (no response body)
400Bad RequestValidation failed, invalid parameters
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but insufficient permissions
404Not FoundResource doesn't exist or no access
409ConflictResource conflict (e.g., duplicate)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error
503Service UnavailableTemporary 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.

Validation Errors
400 Bad Request
{
  "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"
  ]
}
Permission Denied
403 Forbidden
{
  "status": "error",
  "data": [
    "You do not have permission to view invoices"
  ]
}
Authentication Required
401 Unauthorized
{
  "status": "error",
  "data": [
    "Authentication required"
  ]
}
Not Found
404 Not Found
{
  "status": "error",
  "data": [
    "Company not found"
  ]
}
Error Handling
JavaScript
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 IDTypeDescription
1ClientExternal client users with limited access
2ContractorExternal contractors with project access
3StaffInternal staff with full system access
4OtherCustom user type with configurable access

Account Plans

Account plans determine available features:

Plan IDPlanFeatures
1BasicStandard features, single division
3AdvancedTeams, groups, multiple divisions

Permission System

Permissions are identified by numeric IDs and control specific actions:

Permission IDNameControls
2ViewAllWorkSee all work across the account
3ViewItemView tasks and items
4ViewProjectView projects
7ViewTimelogView time entries
8ViewContactNoteView contact/company notes
11ViewSensitiveBusinessInfoView financial/budget fields
12ViewInvoiceView invoices
13ViewQuoteView quotes
14ViewCustomfieldsView custom fields
18ViewUserPermissionsView user permissions
23ViewTeamWorkSee team members' work
24ViewAllWorkDivision-scoped view all

Permission Actions

Each permission supports different actions:

  • view - Read access to resources
  • add - Create new resources
  • edit - Update existing resources
  • delete - Delete resources

Sensitive Field Filtering

Users without ViewSensitiveBusinessInfo (Permission 11) have financial fields automatically hidden:

Affected Resources & Fields:

ResourceHidden Fields
Projectbudget, estBillAmount, invoicedAmount, paidAmount, estProfit, costs
InvoiceestBillAmount, estBillAmountTax, paid, budget, amounts
ProjectItembudget, billAmount, estBillAmount, labour prices
Contactpayrate, payratemultiplier, defaulthourlyrateid
CompanyFinancial 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
  • divisionid parameter 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:

ModeNameVisibility
0ViewAllWorkSee all work (requires permission)
1DefaultSee only own assigned work
2ViewTeamWorkSee team members' work
Permission Check Failed
403 Forbidden
{
  "status": "error",
  "data": [
    "You do not have permission to view invoices"
  ]
}
Sensitive Fields Hidden
200 OK
// User without ViewSensitiveBusinessInfo sees:
{
  "status": "success",
  "data": {
    "id": 123,
    "name": "Project Alpha",
    "status": "active"
    // budget field omitted
    // estBillAmount field omitted
  }
}
Division Filtered Results
cURL
// 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 ValueBehavior
fields=name,code,typeReturns only specified fields (plus id)
fields=allReturns all available fields
omittedReturns default fields for endpoint

Important Notes:

  • The id field 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

TypeFormatExampleNotes
IDint6412345Unique identifier
StringUTF-8 text"Acme Corp"Text fields
DateISO 8601"2024-06-15"Date only
DateTimeISO 8601"2024-06-15T14:30:00"Date and time
Decimalfloat641250.50Money, percentages
HoursDecimal1.51.5 = 1h 30m
BooleanbooltrueTrue/false flags
BinaryHex string"0xABCDEF"Sort order field
ArrayJSON 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):

ResourceSensitive Fields
Projectbudget, estBillAmount, estBillAmountTax, estInTotal, estInTotalTax, estProfit, estProfitPercentage, invoicedAmount, invoicedAmountTax, paidAmount, paidAmountTax, actualCost, actualRevenue, actualProfit
ProjectItembudget, billAmount, estBillAmount, estInTotal, inLabourPrice, outLabourPrice, costPerHour, estCost, estRevenue, estProfit
InvoiceestBillAmount, estBillAmountTax, estIn, estInTax, paid, paidTax, budget, amounts, tax rates
Quotetotal, totalTax, budget, itemized costs
Contactpayrate, payratemultiplier, defaulthourlyrateid, costPerHour, chargeoutrate
Timelogcost, charge, profit, hourlyRate
Expensecost, markup, total

JSON Array Fields

Many resources include arrays of related objects:

FieldStructureAvailable 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:

EntityValid Status Values
Companyactive, inactive, archived
Contactactive, inactive, archived
Projectpending, active, complete, cancelled, archived
Task/Itempending, inprogress, complete, cancelled
Invoicedraft, submitted, authorised, paid, voided
Quotepending, sent, accepted, declined, expired

Computed vs Stored Fields

Stored Fields - Directly from database, support filtering/sorting:

  • id, name, code, status, type
  • createdon, lastmodified
  • startdate, duedate

Computed Fields - Calculated at runtime, may not support filtering:

  • percentcomplete - Calculated from task completion
  • timeremaining - Based on estimates vs actuals
  • profitmargin - Calculated from costs/revenue
  • fullname - Concatenated from firstname + lastname
  • age - Calculated from dates

Examples

Best Practices

  1. Request only needed fields - Reduces payload size and improves performance
  2. Check for field existence - Fields may be omitted based on permissions
  3. Handle null values - Many fields can be null when not set
  4. Parse arrays properly - Array fields may be empty arrays [] or omitted entirely
  5. Respect field types - Don't assume string when API returns numbers
Default Fields
Request
GET /api/v4/companies
# Returns: id, code, name, type (defaults)
Selected Fields
Request
GET /api/v4/projects?fields=name,status,budget,tags
# Returns: id, name, status, budget, tags
# Note: budget may be hidden if user lacks permission
All Fields
Request
GET /api/v4/contacts?fields=all
# Returns all available fields for contacts
# Sensitive fields still subject to permissions
Response with Arrays
JSON
{
  "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.

Update several projects
cURL
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}'
Delete several quotes
cURL
curl -X DELETE 'https://api.proworkflow.com/api/v4/quotes' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-api-key' \
  -d '{"ids": [55, 56]}'
Assign two contacts to three items
cURL
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]}'
Create two time allocations
cURL
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}]'
Bulk response
JSON
{
  "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 ago
  • 5d - 5 days ago
  • 2w - 2 weeks ago
  • 1m - 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:

  1. They are null and marked with omitempty
  2. User lacks permission to view them
  3. They weren't requested via fields parameter
  4. They are empty arrays or objects
String to Integer
JavaScript
// API returns string IDs in some cases
const companyId = parseInt(response.companyid, 10);
Date Parsing
JavaScript
// Parse ISO8601 date
const createdDate = new Date(response.createdon);

// Format for API input
const isoDate = createdDate.toISOString().split('T')[0];
Handle Nullable Fields
JavaScript
// 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.

Search within text fields using various search parameters:

GET /api/v4/companies?search=acme

Searches across default searchable fields (usually name, code, description).

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.

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 sortby and sortorder for consistent ordering
  • Use specific searches: searchcode=ABC is faster than search=ABC
Combined Filters
cURL
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'
Find Overdue Tasks
Request
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
Recent Client Activity
Request
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
High-Value Unpaid Invoices
Request
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