Public API Audits Guide
How to read third-party audit data from the Retraced platform via the Public API.
An audit is an assessment of one facility against one standard, carried out by an audit body: BSCI at a garment factory, SMETA at a fabric mill, GOTS at a dyehouse. It carries the report as a file, the result the auditor awarded, and the date the next one is due. The issues an audit raised become findings, which are worked through in CAPA.
Three read-only endpoints expose them:
GET /api/v2/audits- the audits your company can see.GET /api/v2/audits/{id}- one audit.GET /api/v2/audits/{id}/versions- the other assessments of the same facility against the same standard.
Over the Public API, audits are read-only: every endpoint below is a GET. Audits are uploaded 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 an audit.
The model
audit "amfori BSCI 2026" at Lotus Garments
│ auditTypeId → BSCI carried out 2026-03-18, next due 2027-03-18
│ auditReportFile the PDF the auditor issued
│
├── finding ────────────────► GET /capa/findings?sourceType=AUDIT&sourceId={auditId}
│ "exit lighting broken"
│
└── version series ─────────► GET /audits/{id}/versions
the same facility + standard, year after year
The ids that join them:
| From | Field | Joins to |
|---|---|---|
| Audit | companyId |
The audited company. GET /companies/{id} |
| Audit | auditTypeId |
The standard it was carried out against. See GET /audit-types |
| Audit | uploaderCompanyId |
The company that put it on the platform - often a brand, not the facility |
| Finding | sourceId + sourceType |
The audit behind it: sourceType is AUDIT and sourceId is the audit's id |
⚠️ Related standards are separate audit types, and the id is what tells them apart.
SEDEX_SMETAis "Sedex (SMETA) - 2 Pillar" andGUB825FYUYFis "Sedex (SMETA) - 4 Pillar" - two different assessments that read almost identically by name. Match onauditTypeId, never onauditTypeName, and remember each id is its own version series: a facility assessed against both has two series, not one.
facilityScore is free text, not a scale. Each audit body writes its own result in its own vocabulary, so the column holds "A", "Green", "94", "Approved" and "In Progress" side by side. It is safe to display and to compare within one auditTypeId; it is not comparable across standards and must not be parsed as a number.
Who can see an audit
Three rules, and no others:
- Your company is the one audited (
companyIdis yours), - the audit has been shared with your company, or
- the audit is public and the audited company is connected to yours.
An audit that meets none of the three returns 404 from GET /audits/{id} - the same answer as an id that does not exist, so the API never confirms that someone else's audit exists. It is also simply absent from your list rather than appearing in a redacted form.
There is no public equivalent of the dashboard's browse-the-network-and-request-access flow. Every audit in a response is one you can already read.
List audits
# Everything you can see, most recently changed first
curl "$BASE_URL/audits" -H "companyapikey: $API_KEY"
# One supplier's BSCI audits, newest assessment first
curl "$BASE_URL/audits?companyId=cmp_3Nq8Wl5T&auditTypeId=BSCI&sort=auditDate&order=desc" \
-H "companyapikey: $API_KEY"
# Everything falling due, so you can chase renewals
curl "$BASE_URL/audits?expirationStatus=EXPIRED&latestOnly=true" \
-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, auditDate, nextPlannedAuditDate, name, type, referenceNumber, companyName, uploaderCompanyName, facilityScore or auditedBy. Audits with no value for the chosen field always sort last, whichever direction you choose. |
order |
enum | desc |
asc or desc. |
latestOnly |
true | false |
false |
true returns one audit per company and standard instead of every version - see Versions and renewals. |
companyId |
string | - | The audited company. |
auditTypeId |
string | - | The standard, e.g. BSCI. Exact match on the id, not the display name. |
referenceNumber |
string | - | The certificate or report number the audit body issued. Exact match, case-sensitive. |
visibility |
enum | - | PUBLIC or PRIVATE. |
generatedBy |
enum | - | USER_GENERATED, API or AI_GENERATED. |
auditDateFrom / auditDateTo |
date | - | YYYY-MM-DD, both bounds inclusive, compared in UTC. Audits with no audit date are excluded by either bound. |
expirationStatus |
enum | - | EXPIRED or NON_EXPIRED, compared against nextPlannedAuditDate - see Enumerations. |
isArchived |
true | false |
- | Omit to get both shelves - see Archiving. |
updatedAfterUnixMs / updatedBeforeUnixMs |
number | - | Unix milliseconds, both bounds inclusive. |
Response (200):
{
"metadata": {
"success": true,
"pagination": { "page": 1, "limit": 20, "total": 27, "totalPages": 2 }
},
"data": [
{
"id": "aud_7Rk4Dm2X",
"companyId": "cmp_3Nq8Wl5T",
"companyName": "Lotus Garments Ltd.",
"auditTypeId": "BSCI",
"auditTypeName": "amfori BSCI",
"name": "amfori BSCI 2026",
"type": "Full Audit",
"referenceNumber": "BSCI-2026-118934",
"auditedBy": "Bureau Veritas",
"auditDate": "2026-03-18T09:00:00.000Z",
"nextPlannedAuditDate": "2027-03-18T09:00:00.000Z",
"facilityScore": "B",
"visibility": "PRIVATE",
"isArchived": false,
"externalSource": null,
"generatedBy": "USER_GENERATED",
"digitizationStatus": "COMPLETED",
"digitizationPercentage": 100,
"uploaderCompanyId": "cmp_1Az6Yp0R",
"uploaderCompanyName": "Northwind Apparel",
"auditReportFile": {
"id": "fil_8cKp2Qv1",
"url": "https://files.retraced.co/audits/bsci-2026-lotus.pdf?signature=...",
"contentType": "application/pdf",
"fileType": "AUDIT_REPORT"
},
"correctiveActionPlanFile": null,
"createdAt": "2026-03-20T11:42:07.000Z",
"updatedAt": "2026-04-02T08:15:33.000Z",
"createdByUserId": "usr_5Jd9Hs3B",
"createdByCompanyId": "cmp_1Az6Yp0R",
"createdByImpersonateId": null,
"createdByApiKeyId": null,
"createdByApiKeyLabel": null,
"updatedByUserId": "usr_5Jd9Hs3B",
"updatedByUserName": "Priya Raman",
"updatedByCompanyId": "cmp_1Az6Yp0R",
"updatedByCompanyName": "Northwind Apparel",
"updatedByImpersonateId": null,
"updatedByImpersonateName": null,
"updatedByApiKeyId": null,
"updatedByApiKeyLabel": null
}
]
}
companyNameis what you call that company. It resolves through your own profile of them first and falls back to the name recorded when the audit was created, so two companies can legitimately see different names on the same audit. TreatcompanyIdas the identity andcompanyNameas a label.- The audited company and the uploader are usually different. A brand uploads its supplier's audit, so
uploaderCompanyIdis the brand andcompanyIdis the facility. auditDateandnextPlannedAuditDateare timestamps, and only the date part is meaningful. The platform captures a day; the time of day reflects the timezone the date was entered in rather than an hour anyone chose. Read the date, ignore the clock.createdAtis notauditDate.createdAtis when the record reached Retraced andupdatedAtwhen it was last changed here;auditDateis when the auditor was on site. A report uploaded months after the visit has acreatedAtfar later than itsauditDate, and that is normal.typeis free text recorded on the audit, for example the audit body's own category. Branch onauditTypeId, not on this.digitizationStatusanddigitizationPercentagedescribe structured-data extraction from the report, not the audit itself. Both arenullon the majority of audits, where extraction was never run;digitizationPercentageis 0-100 while it is.externalSourcenames the integration an audit was imported from, for exampleAMFORI. It isnullfor audits created on the platform, which is most of them. Together withgeneratedByit tells you where a record came from:generatedByis the mechanism,externalSourceis the system.
Versions and renewals
A facility audited every year against the same standard produces a series: one audit per year, all with the same companyId and auditTypeId. That pair is the series key.
This is the one place the API deliberately shows you more than the dashboard. The dashboard lists one row per series - the current audit - so a supplier audited against BSCI for the last twenty-seven years is one row on screen and twenty-seven objects here. Neither is wrong; the dashboard answers "what is this supplier's BSCI status", and the API gives you the record.
Three ways to work with that:
# Every version - the default, and what a full sync wants
curl "$BASE_URL/audits?companyId=cmp_3Nq8Wl5T" -H "companyapikey: $API_KEY"
# One per company and standard - what the dashboard shows
curl "$BASE_URL/audits?companyId=cmp_3Nq8Wl5T&latestOnly=true" -H "companyapikey: $API_KEY"
# Walk one series
curl "$BASE_URL/audits/aud_7Rk4Dm2X/versions" -H "companyapikey: $API_KEY"
Which audit is "current" is not simply the newest upload. The platform ranks a series by furthest nextPlannedAuditDate first, then latest auditDate, then most recently created. That is deliberate: uploading an older report late must not displace the audit that is actually still valid. latestOnly=true and /versions both use this order, so you never have to reproduce it.
Three things to know about /versions:
- The audit you asked about is never in its own list. The response holds its siblings - earlier and later assessments - and nothing else. An audit with no siblings returns an empty page, not a
404. - An audit with no
auditTypeIdbelongs to no series and always returns an empty page. Versioning is keyed on the standard; without one there is nothing to group by. - A superseded version is still reachable by its own id.
GET /audits/{id}returns exactly the audit you asked for and never redirects you to the current one.
Fetch an audit
curl "$BASE_URL/audits/aud_7Rk4Dm2X" -H "companyapikey: $API_KEY"
Returns exactly the fields the list returns - there is no wider detail view. 404 when the audit does not exist or is not visible to your company; the two cases are deliberately indistinguishable.
Files
An audit carries up to two documents, each published as an object or null:
| Field | What it is |
|---|---|
auditReportFile |
The report the audit body issued. |
correctiveActionPlanFile |
A corrective action plan document, where one was uploaded as a file. |
Each object carries four fields:
| Field | Notes |
|---|---|
id |
Id of the file record. |
url |
A temporary download link - see below. null when the file record has no stored object behind it. |
contentType |
MIME type, e.g. application/pdf. null when none was recorded on upload. |
fileType |
How the platform classifies the file, e.g. AUDIT_REPORT. null on rows that predate the column. |
⚠️
correctiveActionPlanFileis a document, not the structured plan. The tracked plans, their actions and their progress live in CAPA - seeGET /capa/plans. An audit can easily have both, or either, or neither.
url is a temporary 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; a stored one stops working and cannot be refreshed in place. url is null when the file record has no stored object behind it.
Archiving
Archiving takes an audit off your working list without deleting it. It is per company: an audit a partner archived is still active for you, and archiving never affects what anyone else sees. An audit your own company owns is never archived for you, because the archive shelf belongs to the sharing relationship.
⚠️ Audits differ from every other resource here. On orders, styles and CAPA, omitting
isArchivedgives you the active records only. On audits, omitting it gives you both shelves. SendisArchived=falseexplicitly if you want only the active ones.
isArchived |
Returns |
|---|---|
| omitted | both shelves |
false |
the audits you have not archived |
true |
the audits your company has archived |
See also Archiving vs. deleting.
Record provenance
Every audit says who created it and 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 | What it says |
|---|---|
createdByUserId, createdByCompanyId |
Who created the audit record, and for which company. |
createdByApiKeyId, createdByApiKeyLabel |
The key that created it, and the label that key carried at the time - kept as written even if the key was later renamed or revoked. |
createdByImpersonateId |
Set only when Retraced support created it on your behalf. |
updatedByUserId, updatedByUserName |
Who last changed it. |
updatedByCompanyId, updatedByCompanyName |
Which company that person acted for. On a shared audit this can be a company other than the audited one. |
updatedByApiKeyId, updatedByApiKeyLabel |
The key that made the last change. |
updatedByImpersonateId, updatedByImpersonateName |
Set only when Retraced support made the last change on your behalf. |
Three creator fields are absent rather than null: there is no createdByUserName, createdByCompanyName or createdByImpersonateName on an audit. The record has never stored them, and publishing a key that is permanently null would suggest the data exists. Resolve createdByCompanyId through GET /companies/{id} when you need the name.
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 audit. When they are not null, they are the most important part of the trail.
Enumerations
| Where | Values |
|---|---|
visibility |
PUBLIC (readable by every company connected to the audited company; any other company may request access through the dashboard), PRIVATE (only companies it has been shared with). Not a statement about your own access. |
generatedBy |
USER_GENERATED (uploaded in the dashboard), API (created through an integration), AI_GENERATED |
digitizationStatus |
PROCESSING, COMPLETED, FAILED, or null where extraction was never run |
expirationStatus (filter only) |
EXPIRED, NON_EXPIRED |
sort |
updatedAt, auditDate, nextPlannedAuditDate, name, type, referenceNumber, companyName, uploaderCompanyName, facilityScore, auditedBy |
latestOnly, isArchived |
true, false |
A value outside the documented set is rejected with 400 on every one of these parameters.
expirationStatus is total. EXPIRED means nextPlannedAuditDate is before today; NON_EXPIRED means today or later. An audit due today matches NON_EXPIRED and only that - the boundary belongs to exactly one side, so scanning both values never double-counts and never drops an audit. Audits with no nextPlannedAuditDate match neither and are excluded by either value; list them by omitting the parameter.
Incremental sync
updatedAfterUnixMs and updatedBeforeUnixMs are Unix timestamps in milliseconds and inclusive, so an audit sitting exactly on your cursor comes back again - de-duplicate by id.
# Everything that changed since the last run
curl "$BASE_URL/audits?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:
updatedAtmoves when the audit record changes, not when its findings do. Sync findings in their own pass.- A renewal is a new audit, not an update to the old one. A supplier's next BSCI assessment arrives as a new
idin the same series; the previous audit'supdatedAtdoes not move. Key your store onid, not on(companyId, auditTypeId), or each renewal will overwrite its predecessor.
See Pagination, sorting & incremental sync for the shared paging rules and Rate limits for the request budget.
End-to-end example
# 1. The current audit for each of your suppliers, oldest first so renewals surface
curl -s "$BASE_URL/audits?latestOnly=true&sort=nextPlannedAuditDate&order=asc&limit=100" \
-H "companyapikey: $API_KEY" \
| jq '.data[] | {companyName, auditTypeName, nextPlannedAuditDate, facilityScore}'
# 2. One audit's full history of assessments
AUDIT_ID=$(curl -s "$BASE_URL/audits?latestOnly=true&limit=1" \
-H "companyapikey: $API_KEY" | jq -r '.data[0].id')
curl -s "$BASE_URL/audits/$AUDIT_ID/versions" -H "companyapikey: $API_KEY" \
| jq '.data[] | {auditDate, facilityScore, referenceNumber}'
# 3. What that audit found, and the work raised from it
curl -s "$BASE_URL/capa/findings?sourceType=AUDIT&sourceId=$AUDIT_ID&scope=network" \
-H "companyapikey: $API_KEY" | jq '.data[] | {title, urgency, status}'
# 4. Download the report
curl -s "$BASE_URL/audits/$AUDIT_ID" -H "companyapikey: $API_KEY" \
| jq -r '.data.auditReportFile.url' | xargs curl -o report.pdf
Common errors
| Status | When | How to fix |
|---|---|---|
400 |
An enum value outside the documented set (sort, order, visibility, generatedBy, expirationStatus, latestOnly, isArchived), a malformed YYYY-MM-DD date, a non-numeric …UnixMs, page below 1, or limit outside 1-100. |
Check the parameter table above. |
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 audit data. |
404 |
The audit does not exist, or your company is neither the audited company nor one it was shared with - deliberately indistinguishable. | Verify the id. Confirm the audit is yours or has been shared with you. |
429 |
The key's per-minute allowance is exhausted. | Honour Retry-After and watch X-RateLimit-Remaining - see Rate limits. |
Three behaviours that are not errors and bite silently:
- An id filter that matches nothing returns an empty page with
200, not a404. A typo incompanyId,auditTypeIdorreferenceNumberlooks exactly like "no results". - An unknown query parameter is ignored. A mistyped filter name returns unfiltered data.
- Omitting
isArchivedreturns both shelves, unlike every other resource on this API. A sync that assumes active-only will pick up archived audits.
Related reading
- Getting Started & Integration FAQ - authentication, the response envelope, pagination, rate limits, dates
- CAPA Guide - the findings an audit raised, and the corrective work that resolves them
- Reference data -
GET /audit-typesfor the standards an audit can be carried out against - API Reference - interactive endpoint and schema documentation
- All integration guides