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:

  1. Findings (/api/v2/capa/findings) - the issues an audit or assessment raised.
  2. Actions (/api/v2/capa/actions) - the corrective and preventive work that resolves them.
  3. Plans (/api/v2/capa/plans) - the container that groups actions raised from one source.
  4. 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.

⚠️ Raising a finding on another company's audit does not by itself put it in your network list - 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.id is the same WAGES_AND_BENEFITS-style id you get everywhere else and the impactAreaId filter finds these findings too. A reference the taxonomy cannot place is published as stored, with name null.


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
    }
  ]
}

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/..."
    }
  ]
}

⚠️ url is 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,
  "...": "..."
}

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:

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:

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: