Source: https://atomic-works-test.docs-staging.pageloop.ai/settings/audit-partner-integration

# Audit Partner Integration

# Audit Partner Integration

Sprinto's Audit Partner API lets your integration pull the audits your client has shared with you, review controls, request missing information, and keep your own systems in sync — without a human logging into Sprinto for every step.

This page is for engineers integrating against Sprinto on behalf of an auditor user. It covers the concepts you need before reading the OpenAPI spec, what your API key can and can't do, and the operational details (pagination, sync, errors) you'll hit in practice.

## Core concepts

Sprinto's audit workflow is built from a small number of entities. Skim this before your first call — the field and endpoint names below all map directly onto these.

- **Auditor user** — the individual auditor identity added to an audit. Your API key resolves to one auditor user, and access grants always target that specific identity. If a colleague also needs access, the client adds them as their own separate auditor user.
- **Audit** — one audit engagement (e.g. "SOC 2 Type II — FY2026"). Your key only ever sees audits your auditor user has been explicitly shared on.
- **Control** — the underlying compliance requirement being assessed (e.g. "MFA enforced on production access").
- **Audit Item** — a control or requirement as it's being reviewed _within a specific audit_. This is what you actually work with day to day — one row per item under review in that audit, each carrying its own review status. `auditItemPk` is the ID you'll use across evidence, evidence-review, findings and conversation calls.
  - Each item carries `entityModelName`, which determines what can be attached to it. `OrgEvidenceRequirement` items (evidence-request-list audits) carry requested evidence, mapped policies, manual uploads and checks. `Control` items carry manual uploads and checks only. Read the field rather than assuming a shape — the two are genuinely different, not a bug.
- **Evidence Request** — an ask, raised against a control, for the client to supply a specific piece of supporting material by a due date.
- **Evidence** — the supporting material itself, of four kinds:
  - **Manual evidence** — a file a person on the client side uploaded. Manual evidence is **versioned**: replacing the file records a new version rather than overwriting, and the previous versions stay in the record's history. The version an audit item points at is not always the record's latest — see Evidence.
  - **Automated (system) evidence** — a snapshot automatically captured by one of Sprinto's monitors (e.g. a passing/failing check result). This is what "control check evidence" refers to in the API.
  - **Policy** — an approved policy or procedure document mapped to the item as evidence. Only policies with an approved, published version have a document behind them.
  - **Requested evidence** — an evidence request mapped to the item. The request itself carries no file; the files uploaded in answer to it hang off it separately.
- **Conversation** — a message thread attached to a control (or a control's evidence request), for back-and-forth between you and the client without leaving an audit trail gap.
- **Finding** — an issue, observation or nonconformity you raise during the audit. Findings are recorded against the audit, optionally naming a specific audit item.
- **Evidence sample** — where a check monitors many entities, an auditor may draw a sample and test that subset rather than the population. Once drawn, the sample is fixed, and the API returns it by default.
- **Access grant** — each audit you can see is an explicit grant to your auditor user. Access is revocable at any time by the client, per audit; a revoked audit stops appearing (and returns "not found," not a permission error — see Errors) on your very next call. Revoking your last remaining audit deactivates your API key entirely — it stops authenticating, not just that audit disappearing.

## Status and lifecycle

- **Audit status:** `SCHEDULED` → `EVIDENCE_COLLECTION` → `EVIDENCE_REVIEW` → `DUE_FOR_AUDIT` → `AUDIT_IN_PROGRESS` → `COMPLETED` (or `ARCHIVED`).
- **Audit Item (control) review status:** `COLLECTING_EVIDENCE` → `INTERNAL_REVIEW` → `READY_FOR_REVIEW` → `INFO_REQUESTED` → `ACCEPTED`.
  - **`READY_FOR_REVIEW`** is your queue — controls the client considers done and waiting on you.
  - **`INFO_REQUESTED`** is a control you've flagged as needing more from the client.
  - **`ACCEPTED`** is a control you've formally signed off on. Setting ACCEPTED requires a finding determination — pass evidenceReviewFindingType (e.g. NO\_FINDINGS) or findings text with the status, else it's rejected as "No update required."
- **Evidence Request status:** `UPLOAD_PENDING` → `REVIEW_PENDING` → `REUPLOAD_PENDING` → `UPLOADED`.

## Authentication

Every request carries a per-integration API token in the `api-key` header — there's no `Authorization: Bearer` scheme:

```
api-key: <token>
```

Your token resolves to a dedicated Sprinto user scoped to the `AUDITOR` or `INTERNAL_AUDITOR` role. Every endpoint below checks that role, and separately checks that the specific audit/control/conversation you're asking about is one your key has been granted. The two checks are independent — a valid key with the right role still gets nothing back for an audit it hasn't been granted.

## What you can do

### Read: audits and controls

| Operation                  | Type  | What it returns                                                                                                                                |
| -------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `listAuditsForAuditor`     | Query | Every audit your key has been granted access to. Supports `updatedSince` for incremental sync.                                                 |
| `getAuditForAuditor`       | Query | Details of one shared audit.                                                                                                                   |
| `listAuditItemsForAuditor` | Query | The controls under review within a shared audit, with an optional status filter (see the spec for the exact field) and `updatedSince` support. |

An `auditPk` for an audit that exists but wasn't shared with you returns the same response as one that doesn't exist at all — see Errors.

### Review controls

| Operation                            | Type   | What it does                                                                                                                                                        |
| ------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UPDATE_EVIDENCE_REVIEW_STATUS`      | Action | Moves a single control to a new review status — optionally posting a conversation message in the same call, so a status change and the reason for it land together. |
| `BULK_UPDATE_EVIDENCE_REVIEW_STATUS` | Action | Moves a batch of controls to a new review status in one call (e.g. bulk-accepting everything that's clearly satisfied).                                             |

Both accept `evidenceReviewStatus: "ACCEPTED"` — remember, include evidenceReviewFindingType when accepting; on the bulk action, items already ACCEPTED are skipped and returned in skippedAuditItemPks.

### Conversations

| Operation                   | Type   | What it does                                                                                                  |
| --------------------------- | ------ | ------------------------------------------------------------------------------------------------------------- |
| `getConversationMessages`   | Query  | Reads a thread's message history, including attachments as ready-to-use signed URLs. Supports `updatedSince`. |
| `SEND_CONVERSATION_MESSAGE` | Action | Posts a message to a control's thread — starts one automatically if it doesn't exist yet.                     |

You can only read/post on a conversation whose parent control is on an audit shared with you — not any conversation in the org.

### Evidence on an audit item

| Operation                          | Type  | What it returns                                    |
| ---------------------------------- | ----- | -------------------------------------------------- |
| `listAuditItemEvidencesForAuditor` | Query | Everything attached to one audit item, in one call |

Returns manual uploads, requested evidence, mapped policies and automated/workflow checks as a single list, each row tagged with `evidenceType` (`MANUAL`, `EVIDENCE_REQUESTED`, `POLICIES`, `AUTOMATED`, `WORKFLOW`). This is the auditor's working set for an item — `listAuditItemsForAuditor` gives you the items, this gives you what's on them.

### Evidence

| Operation                      | Type  | What it returns                                                                                                                                                                                                       |
| ------------------------------ | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listManualEvidences`          | Query | Uploaded evidence files.                                                                                                                                                                                              |
| `listControlCheckEvidences`    | Query | A catalogue of which checks have evidence attached, org-wide. Identity only — no results, no evidence rows, no link to an audit item. For what a check actually collected, use `getAuditItemCheckEvidenceForAuditor`. |
| `getManualEvidencesCount`      | Query | How many manual evidence files exist in the org.                                                                                                                                                                      |
| `getManualEvidenceDownloadUrl` | Query | A directly-downloadable, time-limited link to one manual evidence file.                                                                                                                                               |

> Manual evidence is **read-only** for an auditor key. You'll see update, archive and restore actions for manual evidence in the spec — those are customer-only and will return `403`.

All four evidence queries are **org-wide**, not scoped to just your shared audits — as long as you have at least one active grant in the org, you can read every piece of evidence in it. This matches what auditors can already see in Sprinto's own UI; it isn't a narrower or wider carve-out specific to the API. If your only audit in an org gets revoked, this access goes with it.

If you're working a specific audit rather than sweeping the library, prefer `listAuditItemEvidencesForAuditor` — it's scoped to the item, tells you what type each piece of evidence is, and covers policies and checks that these four queries don't reach.

Each `listManualEvidences` node carries, alongside the stored columns:

- `mappedControls` — `[{ pk, number, shortText }]`, the controls this evidence is offered against. This answers "what is this file evidence _for_" without a second call per row.
- `fileName` — the uploaded file's name, or `null` when the record is an external link rather than an upload.
- `version` — the active version number of the record.

**Downloading a file.** `listManualEvidences` returns file metadata and a raw storage key, which is not fetchable on its own. Call `getManualEvidenceDownloadUrl` with the evidence `pk` to get the bytes:

```json
{ "downloadUrl": "https://…s3….amazonaws.com/…?X-Amz-Signature=…", "expiresInSeconds": 1800, "fileName": "access-review-q2-2026.pdf", "s3Key": "controlEvidence/…" }
```

The `downloadUrl` needs **no `api-key` header and no login session** — fetch it directly. It expires after 30 minutes, so mint it when you're ready to download rather than collecting URLs up front, and re-call the query for a fresh one rather than caching.

It resolves the file on the record's **current active version**, so call it again after the client uploads a correction to get the replacement.

It returns `data: null` — not an error — in two distinct cases: the evidence is an external link rather than an upload (read `evidenceFileLink` from the list node instead), and the `pk` isn't in your org. These are deliberately indistinguishable, on the same reasoning as the `400` bucket in Errors. A file blocked by a malware scan is the one case that does error, with a `404`-class response rather than a link to something quarantined.

Download is **one call per evidence record**; there is no batched or ZIP export.

> **`version` is the latest available, not necessarily the version under review.** When a client replaces a file, audit items set to auto-update advance to the new version only if its collection date falls inside that audit's interval and the audit isn't `COMPLETED` or `ARCHIVED`. A correction collected outside the window therefore leaves the audit item on the older version while this query reports the newer one — deliberately, so out-of-period evidence can't silently replace what was reviewed. If you're reconciling against a specific audit rather than sweeping the library, drive off the audit item and treat this field as "latest available".

If you need something from the client that isn't on hand, flag the control (`INFO_REQUESTED`) and message them via the conversation thread — the client sees it in their normal queue.

### Policies

| Operation                                 | Type  | What it does                                                               |
| ----------------------------------------- | ----- | -------------------------------------------------------------------------- |
| `getAuditItemPolicyDownloadUrlForAuditor` | Query | A time-limited link to a policy document mapped to one of your audit items |

Take a `POLICIES` row's `pk` from `listAuditItemEvidencesForAuditor` and pass it with that item's `auditItemPk`. Like manual evidence, the link needs no `api-key` and no session, and expires after 30 minutes. `fileType` accepts `PDF` (default) or `MARKDOWN`.

Returns `data: null` when the policy has no approved version — a policy can be enabled and mapped to an audit item while still being an unpublished draft, which is common. Treat `null` as "no document yet", not an error.

### Automated and workflow check evidence

| Operation                             | Type  | What it does                                                                     |
| ------------------------------------- | ----- | -------------------------------------------------------------------------------- |
| `getAuditItemCheckEvidenceForAuditor` | Query | The rows and headers a check collected, for one check on one of your audit items |

Pass `auditItemPk` plus the `checkType` and `checkCategory` from an `AUTOMATED` or `WORKFLOW` row. Returns a paginated connection of evidence rows with `headers` describing the columns.

**The window is the audit's own evidence-collection period.** An empty result means nothing was collected _inside that period_ — not that the check never ran. A check can be passing today and return nothing here because the audit covers an earlier window. You cannot widen the window; it's derived from the audit.

### Sampling

| Operation                          | Type  | What it does                                                          |
| ---------------------------------- | ----- | --------------------------------------------------------------------- |
| `listAuditSampleResultsForAuditor` | Query | Which evidence sets were sampled on an audit, how many items, and how |

Where a check monitors many entities, an auditor may test a drawn subset instead of the population. **When a sample exists, `getAuditItemCheckEvidenceForAuditor` returns the sample, not the population** — matching what the auditor sees in the app. Pass `includeFullEvidenceSet: true` to read the whole population instead. The `sampling` block on the response tells you which you got, how the sample was drawn, and how many items it holds; it's absent when no sample was drawn.

### Findings

| Operation                     | Type   | What it does                                                        |
| ----------------------------- | ------ | ------------------------------------------------------------------- |
| `listAuditFindingsForAuditor` | Query  | The findings on one of your audits, optionally narrowed to one item |
| `CREATE_FINDING`              | Action | Raises a finding on one of your audits                              |

Findings are recorded against the **audit**, with an audit item as an optional secondary entity — so `listAuditFindingsForAuditor(auditPk)` returns item-level findings too. Pass `auditItemPk` to narrow.

### Audit reports

| Operation                    | Type  | What it does                                                                    |
| ---------------------------- | ----- | ------------------------------------------------------------------------------- |
| `listAuditReportsForAuditor` | Query | Report documents uploaded against one of your audits, with type and upload date |

### Evidence requests

| Operation                     | Type   | What it does                                                                                               |
| ----------------------------- | ------ | ---------------------------------------------------------------------------------------------------------- |
| `CREATE_ORG_EVIDENCE_REQUEST` | Action | Raises a new evidence request (title, assignee, due date) — the client sees it in their normal task queue. |

Creating a request only succeeds when the client org has enabled auditor collaboration with the "can request evidence" capability for you — the same setting that governs this in Sprinto's UI. Without it, the call returns `200` with a permission error in `errors[]`. If that's not enabled for your engagement, flag the control (`INFO_REQUESTED`) and message the client via the conversation thread instead. Editing, archiving, and bulk-creating requests stay admin-only.

## Incremental sync

`listAuditsForAuditor`, `listAuditItemsForAuditor`, `getConversationMessages` and the evidence queries accept `updatedSince` — a full ISO 8601 datetime string:

```bash
SYNC_START=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

curl -s -X POST "{{BASE_URL}}/api/external/schema/queries/listAuditsForAuditor/execute" \
  -H "api-key: $API_KEY" -H "Content-Type: application/json" \
  -d "{ \"args\": { \"first\": 100, \"updatedSince\": \"$SYNC_START\" } }"
```

The audit-item-scoped operations (`listAuditItemEvidencesForAuditor`, `getAuditItemCheckEvidenceForAuditor`, `listAuditFindingsForAuditor`, `listAuditReportsForAuditor`, `listAuditSampleResultsForAuditor`) do not; drive those off an item or audit you already know changed.

Only records with `updatedAt >= updatedSince` come back. Record a timestamp before each sync run and pass it on the next one — a bare date like "2026-06-25" is not rejected with 400 — it's silently interpreted as midnight UTC. Always send a full ISO 8601 datetime.

**`updatedAt` tracks row-level changes only.** Re-mapping a piece of evidence to a different control does **not** bump the evidence record's `updatedAt`, so a mapping-only change on the client side will never appear in an incremental pull. Dedupe by `pk` — the boundary is `>=`, so the same row can arrive twice — and schedule a periodic full resync to pick up mapping changes.

On paginated endpoints (`getConversationMessages`, `listManualEvidences`), passing `updatedSince` switches result ordering to oldest-first instead of the newest-first default, so a row that changes mid-pagination can't slip behind your cursor and get skipped. Ordering is therefore not comparable between calls made with and without it.

**Pagination:** `first` is capped at 100 server-side — ask for more and it's silently clamped, not rejected. Not everything is a paginated connection: `listAuditItemsForAuditor`, `listAuditReportsForAuditor` and `listAuditSampleResultsForAuditor` return plain arrays, and `listAuditItemEvidencesForAuditor` returns a single object with an `evidences` array. `getAuditItemCheckEvidenceForAuditor` and `listAuditFindingsForAuditor` are paginated.

## Errors

| HTTP                          | Shape                                                                                    | When                                                                                                                                                                                                                                                       |
| ----------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `403`                         | `{ "isSuccess": false, "error": "Insufficient permissions..." }`                         | During query, your key's role doesn't match this operation's requirements                                                                                                                                                                                  |
| `403`                         | `{ "success": false, "error": "Insufficient area access for this action" }`              | During action, your key's role doesn't match this operation's requirements                                                                                                                                                                                 |
| `401`                         | `{ "success": false, "error": "User login required!" }`                                  | No valid user resolved from your `api-key`                                                                                                                                                                                                                 |
| `400`                         | `{ "isSuccess": false, "error": "Invalid request. Please check your query arguments." }` | Covers bad arguments **and** an audit/item/conversation that either doesn't exist or wasn't shared with you — these look identical on purpose, so you can't probe what else is on the platform by comparing error messages                                 |
| `200` with a `business error` | `{ "isSuccess": true, "data": { "errors": [...], "data": ... } }`                        | "No update required", or a missing required field) — always check the response errors, not just the HTTP status. The evidence-review  actions return { data, errors } with no top-level success; only the request/conversation actions carry data.success. |

> The two download queries are the exception to the `400` row above. `getManualEvidenceDownloadUrl` returns `isSuccess: true` with `data: null` for a `pk` that isn't in your org, on the same no-probing reasoning, and for evidence that's an external link rather than an upload. `getAuditItemPolicyDownloadUrlForAuditor` returns `null` when the policy has no approved version — a mapped policy can still be an unpublished draft. Treat `null` as "no file available to you", not as a failure to retry.

> Asking for a policy or check that isn't attached to the audit item you named lands in the `400` bucket above, indistinguishable from a bad argument — the mapping is the authorization, so a miss can't be told apart from a typo.

Internal error detail (stack traces, database errors) is never included in a response — check the generic message bucket you landed in above rather than trying to pattern-match on specific wording.

## Best practices

- **Use the filtered spec** (`?tags=audit`) so your integration only builds against operations you'll actually call.
- **Check the action response body, not just the HTTP status** — an action can return 200 and still have rejected your input. The evidence-review actions signal this via an empty/populated errors array (no top-level success field); the evidence-request and conversation actions carry data.success. Check errors in both cases
- **Poll with `updatedSince`, not full re-pulls** — every list endpoint here supports it, and it's how your integration should pick up changes. Pair it with a periodic full resync of evidence, for the reason in Incremental sync.
- **Keep request bodies under 500kb.** This applies across every endpoint, including bulk actions like `BULK_UPDATE_EVIDENCE_REVIEW_STATUS` — batch large jobs into multiple calls rather than one large payload.
- **Pace your integration, especially on sync jobs.** Use `updatedSince` to fetch only what changed instead of re-pulling everything, and space out bulk operations rather than firing large bursts of concurrent requests. Traffic that looks abusive can result in your key being revoked.
- Implement exponential backoff on **429** (respect `Retry-After`).

## FAQ

**Can I see every audit in my client's org?** No — only audits explicitly shared with your specific auditor user. If a colleague needs access, the client adds them as their own auditor user and grants it separately.

**What happens if the client revokes my access mid-sync?** The audit stops appearing in `listAuditsForAuditor` and any direct call for it returns the same "not found" response as an audit that never existed. This takes effect on your very next request — there's no caching delay. If the revoked audit was your last grant, your key is deactivated too — a sudden 401 means "all access revoked," not a transient error.

**How do I flag a control that needs more information from the client?** Set its `evidenceReviewStatus` to `INFO_REQUESTED` via `UPDATE_EVIDENCE_REVIEW_STATUS` and include a `conversationMessage` in the same call — the client sees both the status change and your note together, in their normal queue.

**Can I download evidence files through the API?** Yes. Call `getManualEvidenceDownloadUrl` with an evidence `pk` from `listManualEvidences`. It returns a time-limited link you can fetch directly, with no `api-key` header and no browser session — which is what makes it usable from a sync job or an audit workspace. Links last 30 minutes; re-call the query for a fresh one.

**Is there a bulk export of all evidence?** No. Download is one call per evidence record, and there's no ZIP or archive endpoint. Page through `listManualEvidences`, skip nodes where `fileName` is `null` (those are external links, not uploads), and fetch each file's link as you go rather than minting them all up front.

**Can I get the policy documents mapped to an audit?** Yes. Call `listAuditItemEvidencesForAuditor` for the item, take the `pk` of a `POLICIES` row, and pass it to `getAuditItemPolicyDownloadUrlForAuditor` with the same `auditItemPk`. Policies without an approved version return `null` — there's no document to fetch yet.

**Can I see automated check results?** Yes, per item, via `getAuditItemCheckEvidenceForAuditor`. It returns the evidence rows collected inside the audit period, plus `currentCheckStatus` with Sprinto's passing/failing counts. Those counts are current state and are not scoped to the audit period — see the caveat in that section.

**A check shows no evidence through the API, but I can see it passing in Sprinto — why?** Almost always the audit's evidence-collection window. The audit view answers "what evidence exists inside the audit period"; the Monitoring page shows a check's current state and isn't scoped to any audit. If the check's data was collected outside the audit window, the audit view is empty — for the client's admins too, not just for you. Ask the client to confirm the audit period covers the dates you expect.
