Public API CAPA - Findings, Actions, Plans & History Guide
How to read corrective and preventive action data from the Retraced platform via the Public API.
CAPA is where an audit result turns into work. An audit or an SLCP assessment raises findings; each finding is resolved by actions; actions are grouped into a plan so they can be tracked to closure. Every change any of them goes through is recorded in a history feed.
Four read-only resources expose that:
- Findings (
/api/v2/capa/findings) - the issues an audit or assessment raised. - Actions (
/api/v2/capa/actions) - the corrective and preventive work that resolves them. - Plans (
/api/v2/capa/plans) - the container that groups actions raised from one source. - Histories (
/api/v2/capa/actions/{id}/histories,/api/v2/capa/plans/{id}/histories) - who changed what, and when.
Over the Public API, CAPA is read-only: every endpoint below is a GET. Records are created and changed in the Retraced platform.
Setup
# Base URL
BASE_URL="https://publicapi.retraced.com/api/v2"
# Authentication - use your company API key
API_KEY="your-company-api-key"
All examples use curl. The API key is passed via the companyapikey header, and every response uses the shared envelope described in Getting Started. What you can see is scoped to the company the key belongs to - see Who can see what.
The model
audit / SLCP assessment
│
├── finding ──────────────┐
│ "exit lighting broken" │ resolved by
│ ▼
│ action (CA) "repair the fittings"
│ action (PA) "add it to the checklist"
│ │
└── plan ◄─────────────────┘ grouped under
"Fire safety remediation"
The ids that join them:
| From | Field | Joins to |
|---|---|---|
| Action | findingId |
The finding it was raised to resolve. Also a filter: GET /capa/actions?findingId=… |
| Action | planId |
The plan it belongs to. Also a filter: GET /capa/actions?planId=… |
| Action, plan, finding | sourceId + sourceType |
The audit or SLCP assessment behind it |
| Action, plan, finding | impactArea.id |
The shared sustainability taxonomy, see GET /impact-areas |
| History entry | entityId + entityType |
The action or plan the entry belongs to |
A plan never references a finding directly: the link runs through its actions. That is why GET /capa/plans?findingId=… matches through the actions rather than on a column of the plan.
Some fields are computed, not stored, and they are computed from the records your company can see:
| Field | On | Derived from |
|---|---|---|
status |
Finding, plan | Its actions: TO_DO while none has started, DONE once every one is done, otherwise IN_PROGRESS. Something with no actions reports TO_DO. |
actionCounts, progressPercent |
Plan | The per-status breakdown of its actions, excluding ones you have archived. |
startedAt, endedAt |
Plan | The earliest start and the latest end among its actions. endedAt stays null while any action is still open. |
actionCount, planCount |
Finding | How many actions were raised from it, and how many distinct plans they sit in. |
Two companies can therefore report different status and counts for the same plan - each sees its own slice. A plan's status and progressPercent are live rollups rather than columns, which is also why neither can be sorted or filtered on.
Who can see what
Actions and plans: the company link is the authorization. An action or plan is visible to the company that owns it, the company that created it, and any company collaborating on it. Nobody else can read it, and an id you hold no link to returns 404 - the same answer as an id that does not exist, so the API never confirms that someone else's record exists.
Because the link is what grants access, everyone named on a record you can read is named in full: assignees and reporters carry userId, name and companyId, whichever company they belong to. They are all working on the record with you.
Findings: scope picks the side you are on.
scope=own(the default) - findings your company is responsible for, whoever raised them. This is what an audited facility uses.scope=network- findings belonging to other companies that you can reach. This is what a brand uses to watch its suppliers. Three separate rules decide reachability:- a finding on an audit you can read: one shared with you (an active share of that audit), or a public audit of a company you are connected to. An audit share you archived hides its findings, public or not;
- a finding on an SLCP assessment shared with you;
- a finding of any other source type (
FACTORY_VISIT,OTHER,ZERO_TOLERANCE) that your company raised.
⚠️ Raising a finding on another company's audit does not by itself put it in your
networklist - the share (or the audit being public) does. You can still fetch such a finding directly by id.
The two sets never overlap, so a full picture means calling both.
Two finding fields are narrower still: rootCauseAnalysis and genderedImplications are returned only to the company that raised the finding and the company responsible for it. Everyone else gets null. They are on GET /capa/findings/{id} only - never on the list.
History actors are named by network. An actor from your own company, or from a company yours is connected to, is published with userId and userName. An actor outside that network is published as their company alone (userId and userName are null) - you learn which organisation made the change, not which individual.
Findings
List findings
curl "$BASE_URL/capa/findings?scope=own&page=1&limit=20&sort=updatedAt&order=desc" \
-H "companyapikey: $API_KEY"
Query parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page |
number | 1 |
1-based page number. |
limit |
number | 20 |
Page size, 1-100. |
sort |
enum | updatedAt |
updatedAt, createdAt, dueDate, urgency, title, status, actionCount or planCount. Findings with no value for the chosen field always sort last, in either direction. |
order |
enum | desc |
asc or desc. |
scope |
enum | own |
own or network - see Who can see what. |
sourceType |
enum | - | AUDIT, SLCP, FACTORY_VISIT, OTHER or ZERO_TOLERANCE. |
sourceId |
string | - | The source audit or SLCP assessment. Exact match; an unknown id returns an empty page. |
status |
enum | - | TO_DO, IN_PROGRESS or DONE - matched on the same derived value the response publishes. |
urgency |
enum | - | A (low), B (moderate), C (medium), D (high) or E (extreme). |
impactAreaId |
string | - | An impact area id, e.g. WATER_MANAGEMENT. Also matches SLCP findings whose stored section reference resolves to that id, so the filter agrees with the impactArea the response publishes. |
assigneeUserId |
string | - | Matches assignee.userId. |
companyId |
string | - | The responsible company. Only meaningful with scope=network; on scope=own every finding is already yours and it is ignored. |
isArchived |
true | false |
false |
Your own archive shelf - see Archiving. |
dueDateFrom / dueDateTo |
date | - | YYYY-MM-DD, inclusive. Findings with no due date are excluded by either bound. |
updatedAfterUnixMs / updatedBeforeUnixMs |
number | - | Unix milliseconds, both bounds inclusive. See Incremental sync. |
An invalid enum value, a malformed date or a non-numeric timestamp is rejected with 400. An id that matches nothing returns an empty page with 200.
Response (200):
{
"metadata": {
"success": true,
"pagination": { "page": 1, "limit": 20, "total": 64, "totalPages": 4 }
},
"data": [
{
"id": "afd_9Lp0sD3vNe",
"title": "Emergency exit lighting not working in the cutting hall",
"description": "Two of the six emergency exit lights in the cutting hall were not functional at the time of the audit.",
"urgency": "D",
"status": "IN_PROGRESS",
"dueDate": "2026-10-31T00:00:00.000Z",
"companyId": "cmp_StitchWorks22",
"companyName": "Stitch Works Ltd.",
"createdByCompanyId": "cmp_BrandHouse01",
"createdAt": "2026-08-28T11:30:00.000Z",
"updatedAt": "2026-09-09T16:05:00.000Z",
"actionCount": 2,
"planCount": 1,
"sourceType": "AUDIT",
"sourceId": "aud_3Kd9pQw1Rz",
"origin": "AI_GENERATED",
"impactArea": { "id": "HEALTH_AND_SAFETY", "name": "Health and Safety" },
"assignee": { "userId": "usr_Qa1Zx2Cv3B", "name": "Priya Raman" },
"tags": [{ "id": "tag_Ff8Gg9Hh0J", "name": "Fire safety" }],
"slcp": null,
"isArchived": false
}
]
}
(Response trimmed - every finding also carries the provenance fields saying who created it and who last changed it.)
origin says how the finding came to exist: USER_GENERATED (entered by a person), AI_GENERATED (extracted from an audit report by Retraced), auto_generated (created by the SLCP import) or OTHER.
Fetch a finding
curl "$BASE_URL/capa/findings/afd_9Lp0sD3vNe" -H "companyapikey: $API_KEY"
Returns the same fields plus rootCauseAnalysis and genderedImplications, subject to the rule in Who can see what. Returns 404 when the finding does not exist or is not visible to your company.
SLCP findings
A finding the SLCP import created carries a slcp object with what the assessment recorded for the question behind it. It is null on every other finding.
| Field | Notes |
|---|---|
questionId |
The SLCP question, e.g. wb-min-1. |
questionTitle |
The wording of that question at import time. |
isLegalFlag |
true when the assessment flagged a legal non-compliance, false when it was raised as an inaccuracy. |
response |
The facility's own answer. |
verifiedAnswer |
The verifier's final answer, where the assessment was verified. |
verificationSelection |
How the verifier classified the answer, e.g. "Inaccurate - Incorrect". |
legalReference |
The law or regulation a legal flag refers to. Null on inaccuracy findings. |
{
"id": "afd_2Wq8xR5tYu",
"sourceType": "SLCP",
"sourceId": "slcp_7Hj4kL2mNp",
"origin": "auto_generated",
"impactArea": { "id": "WAGES_AND_BENEFITS", "name": "Wages and Benefits" },
"slcp": {
"questionId": "wb-min-1",
"questionTitle": "Are all workers paid at least the minimum wage?",
"isLegalFlag": true,
"response": "No",
"verifiedAnswer": "No",
"verificationSelection": "Accurate",
"legalReference": "Labour Code art. 92"
},
"...": "..."
}
SLCP assessments record the impact area as a section reference rather than a taxonomy id. The API resolves it for you, so
impactArea.idis the sameWAGES_AND_BENEFITS-style id you get everywhere else and theimpactAreaIdfilter finds these findings too. A reference the taxonomy cannot place is published as stored, withnamenull.
Actions
An action is one piece of work. CA is corrective - fixing what happened. PA is preventive - stopping it happening again.
List actions
# Every open action assigned to a plan
curl "$BASE_URL/capa/actions?planId=cap_5Nw4hY7bKc&status=IN_PROGRESS" \
-H "companyapikey: $API_KEY"
# Everything raised from one finding
curl "$BASE_URL/capa/actions?findingId=afd_9Lp0sD3vNe" \
-H "companyapikey: $API_KEY"
Query parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page |
number | 1 |
1-based page number. |
limit |
number | 20 |
Page size, 1-100. |
sort |
enum | updatedAt |
updatedAt, createdAt, dueDate, status, priority or title. Rows with no value for the chosen field always sort last. |
order |
enum | desc |
asc or desc. |
type |
enum | - | CA or PA. Omit to get both. |
status |
enum | - | TO_DO, IN_PROGRESS, BLOCKED, IN_REVIEW, CHANGES_REQUESTED or DONE. |
priority |
enum | - | A (low) to E (extreme). |
planId |
string | - | Only the actions under this plan. |
findingId |
string | - | Only the actions raised from this finding. |
sourceType |
string | - | AUDIT or SLCP. Not enum-validated: an unrecognised value returns an empty page rather than a 400. |
sourceId |
string | - | The source audit or SLCP assessment. |
impactAreaId |
string | - | An impact area id, e.g. WATER_MANAGEMENT. Exact match on the id, not the display name. |
companyId |
string | - | The company responsible for the action. |
assigneeUserId |
string | - | Matches an assignees[].userId. |
isArchived |
true | false |
false |
Your own archive shelf - see Archiving. |
dueDateFrom / dueDateTo |
date | - | YYYY-MM-DD, inclusive. Actions with no due date are excluded by either bound. |
updatedAfterUnixMs / updatedBeforeUnixMs |
number | - | Unix milliseconds, both bounds inclusive. |
Response (200):
{
"metadata": {
"success": true,
"pagination": { "page": 1, "limit": 20, "total": 37, "totalPages": 2 }
},
"data": [
{
"id": "ca_7Jq2v8LmXt",
"title": "Repair the emergency exit lighting in the cutting hall",
"description": "Two of the six emergency exit lights in the cutting hall were not working during the audit.",
"type": "CA",
"status": "IN_PROGRESS",
"priority": "D",
"sourceType": "AUDIT",
"sourceId": "aud_3Kd9pQw1Rz",
"dueDate": "2026-10-31T00:00:00.000Z",
"startedAt": "2026-09-02T08:15:00.000Z",
"endedAt": null,
"createdAt": "2026-08-28T11:42:00.000Z",
"updatedAt": "2026-09-09T16:05:00.000Z",
"createdByCompanyId": "cmp_BrandHouse01",
"planId": "cap_5Nw4hY7bKc",
"findingId": "afd_9Lp0sD3vNe",
"impactArea": { "id": "HEALTH_AND_SAFETY", "name": "Health and Safety" },
"companyId": "cmp_StitchWorks22",
"companyName": "Stitch Works Ltd.",
"collaboratorCompanyIds": ["cmp_AuditPartner07"],
"assignees": [
{ "userId": "usr_Qa1Zx2Cv3B", "name": "Priya Raman", "companyId": "cmp_StitchWorks22" }
],
"reporters": [
{ "userId": "usr_Mn4Bv5Cx6Z", "name": "Tomás Iglesias", "companyId": "cmp_BrandHouse01" }
],
"tags": [{ "id": "tag_Ff8Gg9Hh0J", "name": "Fire safety" }],
"isArchived": false
}
]
}
companyIdis the company doing the work;createdByCompanyIdis the one that raised it. They are usually different - a brand raises an action for a supplier.collaboratorCompanyIdslists the other companies working on it alongside the responsible one.tagsare yours. Tags belong to the company that created them, so another company's tags on the same action are never returned to you.- Date-like fields are timestamps, not calendar dates.
dueDate,startedAtandendedAtare ISO 8601 instants, so a due date never shifts a day for a caller in another timezone.
Fetch an action
curl "$BASE_URL/capa/actions/ca_7Jq2v8LmXt" -H "companyapikey: $API_KEY"
Returns the same fields plus attachments - the evidence files on the action. The list endpoint never carries them.
{
"attachments": [
{
"id": "fil_Ww1Ee2Rr3T",
"name": "exit-lighting-invoice.pdf",
"contentType": "application/pdf",
"fileSize": 184320,
"createdAt": "2026-09-09T15:58:00.000Z",
"url": "https://objectstorage.example/p/READ-ONLY-TOKEN/n/retraced/b/private/o/..."
}
]
}
⚠️
urlis a temporary read-only download link, valid for one hour from the moment the response was generated. It grants read access to that one file and nothing else. Fetch a fresh response rather than storing the link.
Returns 404 when the action does not exist or your company holds no link to it. A plan id passed here is also a 404 - plans have their own endpoint.
Plans
A plan groups the actions raised from one audit or assessment so they can be tracked to closure.
List plans
curl "$BASE_URL/capa/plans?sort=dueDate&order=asc&limit=20" \
-H "companyapikey: $API_KEY"
Query parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page |
number | 1 |
1-based page number. |
limit |
number | 20 |
Page size, 1-100. |
sort |
enum | updatedAt |
updatedAt, createdAt, dueDate, priority, title or actionsCount. No sort by status or progress - both are live rollups over the actions, not stored columns. |
order |
enum | desc |
asc or desc. |
priority |
enum | - | A (low) to E (extreme). |
sourceType |
string | - | AUDIT or SLCP. Not enum-validated. |
sourceId |
string | - | The source audit or SLCP assessment. |
impactAreaId |
string | - | An impact area id. |
companyId |
string | - | The company responsible for the plan. |
assigneeUserId |
string | - | Matches an assignees[].userId. |
findingId |
string | - | Plans with at least one action raised from this finding. |
isArchived |
true | false |
false |
Your own archive shelf. |
dueDateFrom / dueDateTo |
date | - | YYYY-MM-DD, inclusive. |
updatedAfterUnixMs / updatedBeforeUnixMs |
number | - | Unix milliseconds, both inclusive. |
Response (200):
{
"id": "cap_5Nw4hY7bKc",
"title": "Fire safety remediation - Stitch Works, March audit",
"status": "IN_PROGRESS",
"priority": "D",
"sourceType": "AUDIT",
"sourceId": "aud_3Kd9pQw1Rz",
"dueDate": "2026-11-30T00:00:00.000Z",
"startedAt": "2026-09-02T08:15:00.000Z",
"endedAt": null,
"createdAt": "2026-08-28T11:40:00.000Z",
"updatedAt": "2026-09-09T16:05:00.000Z",
"createdByCompanyId": "cmp_BrandHouse01",
"impactArea": { "id": "HEALTH_AND_SAFETY", "name": "Health and Safety" },
"companyId": "cmp_StitchWorks22",
"companyName": "Stitch Works Ltd.",
"collaboratorCompanyIds": ["cmp_AuditPartner07"],
"actionCounts": {
"toDo": 1,
"inProgress": 2,
"blocked": 0,
"inReview": 0,
"changesRequested": 0,
"done": 1,
"total": 4
},
"progressPercent": 25,
"isArchived": false,
"...": "..."
}
actionCountsis your view of the plan: it counts only the actions you can see and excludes the ones you have archived.progressPercentisdoneas a whole-number percentage oftotal, and isnull- not0- for a plan with no actions.updatedAtmoves when the plan itself changes, not when one of its actions does. An incremental sync on plans alone will not see action-level activity: page the actions too, or use the history feeds.- List the plan's actions with
GET /capa/actions?planId={id}.
Fetch a plan
curl "$BASE_URL/capa/plans/cap_5Nw4hY7bKc" -H "companyapikey: $API_KEY"
Adds attachments - the files on the plan itself, with the same one-hour read-only links as actions. An action's own attachments come from GET /capa/actions/{id}. An action id passed here returns 404.
History
Every change to an action or a plan is recorded, newest first:
curl "$BASE_URL/capa/actions/ca_7Jq2v8LmXt/histories?page=1&limit=20" \
-H "companyapikey: $API_KEY"
curl "$BASE_URL/capa/plans/cap_5Nw4hY7bKc/histories" \
-H "companyapikey: $API_KEY"
Query parameters: page (default 1), limit (1-100, default 20), and createdAfterUnixMs / createdBeforeUnixMs - Unix milliseconds, both bounds inclusive. There is no sort: entries are always newest first.
Response (200):
{
"metadata": {
"success": true,
"pagination": { "page": 1, "limit": 20, "total": 4, "totalPages": 1 }
},
"data": [
{
"id": "hst_7Hn2Kp9Qw1",
"entityId": "ca_7Jq2v8LmXt",
"entityType": "ACTION",
"changeType": "added",
"field": "assignees",
"oldValue": null,
"newValue": {
"userId": "usr_Zt5Yu6Io7P",
"name": "Daniel Osei",
"companyId": "cmp_StitchWorks22",
"companyName": "Stitch Works Ltd."
},
"actor": {
"companyId": "cmp_BrandHouse01",
"companyName": "Brand House GmbH",
"userId": "usr_Qa1Zx2Cv3B",
"userName": "Priya Raman"
},
"createdAt": "2026-09-09T16:05:00.000Z"
},
{
"id": "hst_3Fl0Im7Ou9",
"entityId": "ca_7Jq2v8LmXt",
"entityType": "ACTION",
"changeType": "changed",
"field": "status",
"oldValue": "TO_DO",
"newValue": "IN_PROGRESS",
"actor": {
"companyId": "cmp_AuditCo77",
"companyName": "Audit Co. Ltd.",
"userId": null,
"userName": null
},
"createdAt": "2026-08-30T08:00:00.000Z"
}
]
}
changeType says what happened: created (the record appeared), changed (a field took a new value), added or removed (an item joined or left a collection), deleted (the record was deleted). field is null on created and deleted entries.
field names what changed, and decides the shape of oldValue / newValue:
field |
The value holds |
|---|---|
title, description |
The text. |
status |
TO_DO, IN_PROGRESS, BLOCKED, IN_REVIEW, CHANGES_REQUESTED or DONE. |
priority |
A to E. |
dueDate, startedAt, endedAt |
An ISO 8601 timestamp. |
impactAreaId |
An impact area id. |
sourceType, sourceId |
The source of the record. |
type |
CA, PA or CAP. |
isArchived |
1 when archived, 0 when not. |
assignees, reporters |
A person: { userId, name, companyId, companyName }. The user parts are null outside your network. |
collaborators, owner |
A company: { companyId, companyName }. |
linkedActions |
{ actionId }. |
attachments |
{ fileId }. |
remediationRequest |
{ message }. |
comment |
Always null - comments are not published over the API. |
The second entry in the example above shows the network rule from Who can see what: the change was made at a company yours is not connected to, so the entry names the company and leaves userId and userName null.
The actor.companyName is the name your company recorded for that company where you have one, and the company's own name otherwise.
The two feeds are separate: an action's history carries only that action's changes, and a plan's only the plan's own. Changes to the actions under a plan are on each action.
Record provenance
Every CAPA record says who created it and, where the data exists, who last changed it. This is your own audit trail: use it to reconcile what your integration wrote against what people changed in the dashboard.
Each write records one channel. A change made in the Retraced dashboard fills the user fields and
leaves the API-key ones null; a change made with an API key does the opposite. So a null here
means "not that channel", not "unknown".
| Field | On |
|---|---|
createdByUserId, createdByCompanyId |
findings, actions, plans |
createdByCompanyName |
actions, plans |
createdByUserName |
findings |
createdByImpersonateId, createdByApiKeyId, createdByApiKeyLabel |
findings |
updatedByUserId, updatedByUserName, updatedByCompanyId, updatedByCompanyName |
findings |
updatedByImpersonateId, updatedByImpersonateName |
findings |
updatedByApiKeyId, updatedByApiKeyLabel |
findings |
Why actions and plans carry less: they share one table, and it records only the creator. There is no updater, API-key or impersonation column to publish. For those two resources, the change history is the place to look - see History, which names the actor on every entry.
The
*Impersonate*fields are set when Retraced staff acted on your company's behalf through support. They are null on everything your own people and keys did, which is almost every record. When they are not null, they are the most important part of the trail.
Archiving
Archiving is a reversible way to take a record off the working list without deleting it. Across CAPA it is per company, and there are exactly two shelves:
- Omit
isArchived, or sendisArchived=false- the active records. This is the default on all three list endpoints. - Send
isArchived=true- the records your company has archived instead.
The two sets never overlap, and there is no "both" mode. A record a partner company archived is still active for you, and archiving never affects what they see. Nothing is ever deleted over this API. See also Archiving vs. deleting.
Enumerations
| Where | Values |
|---|---|
Action type |
CA (corrective), PA (preventive). CAP appears only as a history type value, where it means the plan. |
Action status |
TO_DO, IN_PROGRESS, BLOCKED, IN_REVIEW, CHANGES_REQUESTED, DONE |
Finding and plan status |
TO_DO, IN_PROGRESS, DONE (derived - see The model) |
priority and finding urgency |
A low, B moderate, C medium, D high, E extreme |
Finding sourceType |
AUDIT, SLCP, FACTORY_VISIT, OTHER, ZERO_TOLERANCE |
Action and plan sourceType |
AUDIT, SLCP |
Finding origin |
USER_GENERATED, AI_GENERATED, auto_generated, OTHER |
History changeType |
created, added, changed, removed, deleted |
History entityType |
ACTION, PLAN |
A value outside the documented set is rejected with 400 on every parameter except sourceType on actions and plans, which is matched as free text and returns an empty page instead.
Incremental sync
Findings, actions and plans each expose updatedAfterUnixMs; the history feeds expose createdAfterUnixMs. All bounds are Unix timestamps in milliseconds and inclusive, so a record sitting exactly on your cursor comes back again - de-duplicate by id.
# Everything that changed since the last run
curl "$BASE_URL/capa/actions?updatedAfterUnixMs=1788307200000&sort=updatedAt&order=asc&limit=100" \
-H "companyapikey: $API_KEY"
Store the largest updatedAt you have seen and pass it on the next run. Pair it with sort=updatedAt&order=asc so a long sync pages forward through the changes in order.
Two things worth planning for:
- A plan's
updatedAtdoes not move when its actions change. Sync actions in their own pass, or follow the history feeds. - Archiving is a change like any other: a record you archive leaves the default list, so a sync that only ever reads the active shelf will see it disappear rather than change. Poll
isArchived=trueas well if you need to track that.
See Pagination, sorting & incremental sync for the shared paging rules and Rate limits for the request budget.
End-to-end example
# 1. The findings your company is responsible for, still open
FINDING_ID=$(curl -s "$BASE_URL/capa/findings?scope=own&status=IN_PROGRESS&limit=1" \
-H "companyapikey: $API_KEY" | jq -r '.data[0].id')
# 2. The actions raised to resolve one of them
curl -s "$BASE_URL/capa/actions?findingId=$FINDING_ID&limit=100" \
-H "companyapikey: $API_KEY" | jq '.data[] | {id, title, status, dueDate}'
# 3. The plan those actions sit under, with its rollup
PLAN_ID=$(curl -s "$BASE_URL/capa/actions?findingId=$FINDING_ID&limit=1" \
-H "companyapikey: $API_KEY" | jq -r '.data[0].planId')
curl -s "$BASE_URL/capa/plans/$PLAN_ID" -H "companyapikey: $API_KEY" \
| jq '.data | {title, status, actionCounts, progressPercent}'
# 4. Who changed what on the first action
ACTION_ID=$(curl -s "$BASE_URL/capa/actions?findingId=$FINDING_ID&limit=1" \
-H "companyapikey: $API_KEY" | jq -r '.data[0].id')
curl -s "$BASE_URL/capa/actions/$ACTION_ID/histories" -H "companyapikey: $API_KEY" \
| jq '.data[] | {createdAt, changeType, field, actor: .actor.companyName}'
Common errors
| Status | When | How to fix |
|---|---|---|
400 |
An enum value outside the documented set (status, priority, urgency, type, scope, sort, order, finding sourceType), a malformed YYYY-MM-DD date, a non-numeric …UnixMs, page below 1, or limit outside 1-100. |
Check the parameter tables above. Note action and plan sourceType are the exception: they are matched as free text. |
401 |
Missing or invalid credentials (token_or_api_key_invalid). |
Check the companyapikey header name (all lowercase) and the key value. |
403 |
Authenticated, but the credential does not resolve to a company (insufficient_rights). |
Use a company API key; a key not bound to a company cannot read CAPA data. |
404 |
The record does not exist, or your company has no link to it - deliberately indistinguishable. Also returned when the id is the wrong kind: a plan id on an action endpoint, or an action id on a plan endpoint. | Verify the id and the endpoint. Confirm your company owns, created or collaborates on the record. |
429 |
The key's per-minute allowance is exhausted. | Honour Retry-After and watch X-RateLimit-Remaining - see Rate limits. |
Two behaviours that are not errors and bite silently:
- An id filter that matches nothing returns an empty page with
200, not a404. A typo inplanId,findingId,sourceId,impactAreaId,companyIdorassigneeUserIdlooks exactly like "no results". - An unknown query parameter is ignored. A mistyped filter name returns unfiltered data.
Related reading
- Getting Started & Integration FAQ - authentication, the response envelope, pagination, rate limits, dates
- Reference data -
GET /impact-areasfor the sustainability taxonomy andGET /audit-typesfor audit standards - API Reference - interactive endpoint and schema documentation
- All integration guides