AgenciesWP REST API Documentation
The AgenciesWP REST API lets you read and write your agency data from your own systems, or from Zapier, n8n and Make through their HTTP modules. Simple keys, scoped permissions, and nothing is ever deleted.
Overview
The AgenciesWP REST API lets you read and write your clients, projects, invoices, time logs and more from your own systems, or from Zapier, n8n and Make through their HTTP modules. It runs inside your own WordPress site, so your data never passes through a third-party service.
Messages and the activity log are not available through the API.
Base URL
Every request goes to the REST namespace on your own site:
https://your-site.com/wp-json/agencieswp/v1Creating a key
In WordPress admin, go to Agencies WP → Settings → API. Give the key a label you will recognise later, such as “Zapier” or “Our CRM”, tick the permissions it needs, and save.
The key is shown once. It is stored hashed, so it cannot be shown again. If it is lost, revoke it and make another.
A key looks like this:
awp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKeys can be revoked at any time. A revoked key stops working immediately.
Authenticating
Send the key in a header. Either of these works:
Authorization: Bearer awp_your_key_hereX-AgenciesWP-Key: awp_your_key_hereNever put the key in the query string. The API rejects it there, because URLs end up in server logs, browser history and referrer headers.
HTTPS is required. A key sent over plain HTTP can be read by anything between you and the site, so requests over plain HTTP are refused outright. There is no setting to turn that off on a live site.
Checking it works
curl -s https://your-site.com/wp-json/agencieswp/v1/
-H "Authorization: Bearer awp_your_key_here"{
"name": "AgenciesWP API",
"version": "1.2",
"namespace": "agencieswp/v1",
"key": "Zapier",
"scopes": ["read:clients", "read:projects", "write:projects"],
"notes": "This API never deletes. There are no delete routes.",
"endpoints": { "...": "..." }
}The index tells you which key you are using and exactly what it can reach, which makes it the quickest way to debug a permission error.
Permissions
A key holds whichever of these you grant it. Read permissions expose data. Write permissions create and update records, and never delete them.
| Permission | What it allows |
|---|---|
read:clients | Read clients |
read:projects | Read projects |
read:invoices | Read invoices |
read:subscriptions | Read recurring invoices |
read:proposals | Read proposals |
read:time | Read time logs |
read:milestones | Read milestones |
read:expenses | Read expenses |
read:stats | Read summary figures |
write:clients | Create and update clients |
write:projects | Create and update projects |
write:invoices | Create and update invoices |
write:time | Create and update time logs |
write:milestones | Create and update milestones |
write:expenses | Create and update expenses |
Grant the narrowest set that does the job. A key for a reporting dashboard needs no write permission at all.
Endpoints
Reading
| Method | Path | Permission |
|---|---|---|
GET | /clients | read:clients |
GET | /clients/{id} | read:clients |
GET | /projects | read:projects |
GET | /projects/{id} | read:projects |
GET | /invoices | read:invoices |
GET | /invoices/{id} | read:invoices |
GET | /subscriptions | read:subscriptions |
GET | /subscriptions/{id} | read:subscriptions |
GET | /proposals | read:proposals |
GET | /proposals/{id} | read:proposals |
GET | /time-logs | read:time |
GET | /time-logs/{id} | read:time |
GET | /milestones | read:milestones |
GET | /milestones/{id} | read:milestones |
GET | /expenses | read:expenses |
GET | /expenses/{id} | read:expenses |
GET | /stats | read:stats |
Writing
| Method | Path | Permission |
|---|---|---|
POST | /clients | write:clients |
PATCH / PUT | /clients/{id} | write:clients |
POST | /projects | write:projects |
PATCH / PUT | /projects/{id} | write:projects |
POST | /invoices | write:invoices |
PATCH / PUT | /invoices/{id} | write:invoices |
POST | /time-logs | write:time |
PATCH / PUT | /time-logs/{id} | write:time |
POST | /milestones | write:milestones |
PATCH / PUT | /milestones/{id} | write:milestones |
POST | /expenses | write:expenses |
PATCH / PUT | /expenses/{id} | write:expenses |
Recurring invoices and proposals are read-only. Both have schedules and signatures behind them that should be set up in the admin.
Listing and pagination
Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
per_page | integer | 25 | How many records per page |
page | integer | 1 | Which page |
status | string | — | Filter by status, where the object has one |
search | string | — | Free-text search, where the object supports it |
client_id | integer | — | Only records for this client |
project_id | integer | — | Only records for this project. Required for /milestones and /time-logs. Both are always read per project, and leaving it out returns 400 project_required |
Pagination headers
Every list response carries these headers. Page through until page reaches X-WP-TotalPages.
X-WP-Total: 143
X-WP-TotalPages: 6Example
curl -s "https://your-site.com/wp-json/agencieswp/v1/invoices?status=overdue&per_page=50"
-H "Authorization: Bearer awp_your_key_here"[
{
"id": 42,
"invoice_number": "INV-0042",
"client_id": 7,
"project_id": 11,
"subscription_id": 0,
"amount": 4000.00,
"tax": 800.00,
"discount": 0.00,
"total": 4800.00,
"paid_amount": 0.00,
"status": "overdue",
"payment_method": "",
"due_date": "2026-09-30",
"paid_date": null,
"notes": "",
"items": [
{ "description": "Website rebuild - phase 1", "qty": 1, "rate": 4000.00 }
],
"created_at": "2026-09-01 10:14:02",
"updated_at": "2026-10-01 00:00:11"
}
]A list is a plain JSON array. A single record is a plain JSON object. There is no wrapper envelope.
Fields
Each object returns an explicit list of fields. It is an allow-list, not a blocklist, so a column added to the database in a later release stays invisible to the API until it is deliberately published. Secrets are never on these lists.
clients
id unique_id name email phone company title website address notes status billing_address country tax_id billing_email created_at updated_at
projects
id title description client_id status progress budget budget_type hourly_rate start_date due_date estimated_hours project_code service_type lead_user_id lead_name created_at updated_at
invoices
id invoice_number client_id project_id subscription_id amount tax discount total paid_amount status payment_method due_date paid_date notes items created_at updated_at
subscriptions (recurring invoices)
id title client_id project_id items amount tax discount total frequency start_date next_run_date end_date max_cycles cycles_completed due_days auto_send status last_invoice_id created_at updated_at
proposals
id proposal_number title recipient_name recipient_email recipient_company amount tax discount total status valid_until client_id project_id sent_at viewed_at responded_at created_at updated_at
time-logs
id project_id user_id log_date hours hourly_rate billable description created_at
milestones
id project_id title description status due_date assigned_to sort_order created_at
expenses
id description vendor category project_id client_id amount tax total expense_date status billable invoice_id receipt_url notes created_at updated_at
The items field on invoices, recurring invoices and proposals is stored as JSON and returned as real JSON, not a string.
Creating records
Send JSON with Content-Type: application/json. Form-encoded bodies work too.
A client
curl -s -X POST https://your-site.com/wp-json/agencieswp/v1/clients
-H "Authorization: Bearer awp_your_key_here"
-H "Content-Type: application/json"
-d '{
"name": "Harbor Fintech",
"email": "[email protected]",
"company": "Harbor Fintech Ltd",
"country": "GB",
"tax_id": "GB123456789"
}'A successful create returns 201 with the new record, including the id the system assigned:
{ "id": 18, "unique_id": "CL100018", "name": "Harbor Fintech", "...": "..." }unique_id is generated for you. It is the reference the client uses to sign in to their portal, so it cannot be supplied.
A project
curl -s -X POST https://your-site.com/wp-json/agencieswp/v1/projects
-H "Authorization: Bearer awp_your_key_here"
-H "Content-Type: application/json"
-d '{
"title": "Website rebuild",
"client_id": 18,
"status": "in_progress",
"budget_type": "fixed",
"budget": 12000,
"due_date": "2027-02-28",
"service_type": "Web design",
"estimated_hours": 140
}'For an hourly project, send "budget_type": "hourly" with an hourly_rate. The budget then follows the hours logged against it.
A time log
curl -s -X POST https://your-site.com/wp-json/agencieswp/v1/time-logs
-H "Authorization: Bearer awp_your_key_here"
-H "Content-Type: application/json"
-d '{
"project_id": 11,
"log_date": "2026-10-28",
"hours": 3.5,
"billable": 1,
"description": "Checkout flow QA"
}'Updating records
Use PATCH (or PUT) with only the fields you want changed. Anything you leave out keeps its current value.
curl -s -X PATCH https://your-site.com/wp-json/agencieswp/v1/invoices/42
-H "Authorization: Bearer awp_your_key_here"
-H "Content-Type: application/json"
-d '{ "status": "paid", "paid_amount": 4800.00, "paid_date": "2026-10-29" }'It returns 200 with the updated record.
Summary figures
One call returns the headline numbers, which is handy for a dashboard tile without paging through every record.
curl -s https://your-site.com/wp-json/agencieswp/v1/stats
-H "Authorization: Bearer awp_your_key_here"{
"clients": { "total": 18 },
"projects": {
"total": 24, "pending": 2, "in_progress": 9, "completed": 13, "on_hold": 0
},
"invoices": {
"total_count": 96,
"total_invoiced": 184300.00,
"total_paid": 171900.00,
"total_due": 12400.00,
"overdue_count": 2
},
"recurring": { "total": 7, "active": 6, "paused": 1, "mrr": 4180.00 },
"currency": "USD"
}mrr is monthly recurring revenue, with each plan’s amount normalised to a month. A quarterly plan counts as a third of its value, and a yearly one as a twelfth.
Errors
Errors come back in WordPress’s standard shape, with a matching HTTP status:
{
"code": "insufficient_scope",
"message": "This API key does not have the "write:invoices" permission.",
"data": { "status": 403 }
}| Status | Code | Meaning |
|---|---|---|
401 | missing_key | No key in the request headers |
401 | invalid_key | The key is not valid. Check it was copied in full and has not been revoked |
403 | https_required | The request came over plain HTTP |
403 | insufficient_scope | Valid key, but without the permission this route needs |
404 | not_found | No record with that id |
404 | unknown_route | No such resource |
400 | project_required | /milestones or /time-logs was listed without a project_id |
400 | duplicate_email | A client with that email address already exists |
400 | missing_name | A required field was not supplied |
400 | other | The record could not be created or updated. The message says what was wrong |
429 | rate_limited | Over the per-minute allowance |
Rate limit
The limit is 120 requests per minute per key. It is counted per key rather than per IP, so one integration cannot exhaust another’s allowance.
Going over returns 429. Wait for the next minute and continue. If you are importing in bulk, space the requests out, or page with a larger per_page instead of many small requests.
Integration tips
- Use one key per integration. If one has to be revoked, nothing else stops working.
- Grant the narrowest permissions that do the job. A key that only reads invoices cannot touch a client record, even if it is leaked.
- Store the key as a secret, not in a repository or a shared document.
- Use
GET /as your health check. It confirms the key works and lists exactly what it can reach, which is faster than guessing at a403. - Nothing here deletes. If a record needs removing, do it in the WordPress admin.
project_idis required on/milestonesand/time-logs. Both are always read per project. There is no “all milestones” list.- Client email addresses are unique. Creating a client with an address that already exists returns
400 duplicate_emailrather than a second record, so an import that runs twice does not produce duplicates.
Support
Questions, or something not behaving as documented? Get in touch and I will help.
Ready to automate your agency?
The REST API is built into AgenciesWP. Create a key in Settings and start connecting.
See pricingBack to documentation