> For the complete documentation index, see [llms.txt](https://docs.sprinto.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sprinto.com/settings/audit-partner-integration.md).

# 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 as it's being reviewed *within a specific audit*. This is what you actually work with day to day — one row per control under review in that audit, each carrying its own review status. `auditItemPk` is the ID you'll use across evidence-review and conversation calls.
* **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 two 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.
* **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.
* **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

| Operation                      | Type  | What it returns                                                         |
| ------------------------------ | ----- | ----------------------------------------------------------------------- |
| `listManualEvidences`          | Query | Uploaded evidence files.                                                |
| `listControlCheckEvidences`    | Query | Automated check evidence behind passing/failing controls.               |
| `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.

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.

### 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

Every list-shaped operation above accepts `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\" } }"
```

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. `listAuditItemsForAuditor` returns a plain array, not a paginated connection.

## 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": ... } }`                        | <p>"No update required", or a missing required field) — always check the response errors, not just the HTTP status. The evidence-review</p><p>  actions return { data, errors } with no top-level success; only the request/conversation actions carry data.success.</p> |

> `getManualEvidenceDownloadUrl` is the one exception to the `400` row above: an evidence `pk` that isn't in your org returns `isSuccess: true` with `data: null` rather than a `400`, on the same no-probing reasoning. Treat `null` as "no file available to you", not as a failure to retry.

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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.sprinto.com/settings/audit-partner-integration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
