Migrate audit calls from v1

v1 has three audit calls: GET /v1/audits/:audit_id returns one audit, POST /v1/audits lists audits that match a filter body, and POST /v1/update-audits-flags adds flags to audits. In v2, GET /v2/audits/{audit_id} returns one audit and GET /v2/audits lists them with query parameters; PATCH /v2/audits/{audit_id} and POST /v2/audits/batch_update set flags.

A direct port of the list returns 20 audits, not 1,000. v1 POST /v1/audits returns up to pageSize audits (default 1,000) in one array. v2 GET /v2/audits returns 20 per page by default and at most 100, and sets has_more to true when more follow. Loop with starting_after until has_more is false.

Migration checklist

Copy this list into a ticket. Each item is one change to the code that calls the v1 audit endpoints.

  • Send requests to https://api.brellium.com/v2/... (v1 stays at /v1/...), with a v2 access token (Authenticate with OAuth2 client credentials).
  • Fetch one audit with GET /v2/audits/{audit_id} instead of GET /v1/audits/:audit_id. Remove the includeDisplayInfo, includePeople, includeQuestionIds and includeLastReviewed query parameters: v2 always returns every field.
  • List audits with GET /v2/audits and query parameters instead of POST /v1/audits with a JSON body. Rename the filters: lastUpdatedStartDate/lastUpdatedEndDate become updated_after/updated_before, and auditStartDate/auditEndDate become created_after/created_before. documents, flags, status and set_id keep their names; status takes one value, not an array.
  • Send dates as RFC 3339 timestamps in UTC ending in Z, such as 2026-09-01T00:00:00Z. An offset such as +02:00 or a date alone (2026-09-01) returns 400 VALIDATION_ERROR.
  • Replace page and pageSize with cursor pagination: limit (1 to 100, default 20) and starting_after set to the last audit_id of the previous page. Read audits from data and stop when has_more is false.
  • Remove verbose, includeResponses and includeTestAudits. v2 always returns score, flags and responses. It never returns the test audits that v1 returns with includeTestAudits, and it includes audits of coding question sets, which v1 lists leave out.
  • Filter by score in your own code, because v2 has no equivalent of scoreLowerBound and scoreUpperBound. sessionStartDate/sessionEndDate have no v2 equivalent either.
  • Fetch related records from their own endpoints. An audit carries only their ids: set_id, documents (a list of document_id values) and people[].person_id. people_emails is gone, so fetch each person's email address from GET /v2/email_recipients/{person_id}.
  • Rename the response fields: timestamp becomes created_at, last_updated becomes updated_at, last_reviewed becomes reviewed_at, and in each item of responses, query becomes question and context becomes evidence. In people[], key, title and value become role, role_title and name. reviewed_by, metadata, document_id, people_emails and responses[].external_key are gone.
  • Every field is present on every status. Check status === "complete" before using score, flags or responses; do not check whether the field exists.
  • Change flag updates from POST /v1/update-audits-flags to PATCH /v2/audits/{audit_id} (one audit) or POST /v2/audits/batch_update (up to 100). v2 replaces the flags with the list you send; v1 adds them. Fetch the audit, add your flag to its current list, then send the full list. Do not send the same flag twice in one list.
  • Replace a v1 flag update by filter with a list call followed by batch_update.
  • Update your error handling for v2. Every error response is one JSON envelope: { "error": { "type", "code", "message", "param"?, "details"?, "request_id" } }. Branch on type and code, which are stable; message and details can change. Handle 400 VALIDATION_ERROR for a bad parameter, 404 NOT_FOUND for an unknown audit, and 429 RATE_LIMITED for too many requests (wait Retry-After seconds, then retry).

Retrieve an audit

GET /v1/audits/:audit_id becomes GET /v2/audits/{audit_id}. The audit_id value is unchanged.

Request

- GET https://<your v1 host>/v1/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
-     ?includeDisplayInfo=true&includePeople=true
-     &includeQuestionIds=true&includeLastReviewed=true
+ GET https://api.brellium.com/v2/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
  Authorization: Bearer $BRELLIUM_ACCESS_TOKEN
v1 query parameter v2 Notes
includeDisplayInfo=true no parameter display_info is always present
includePeople=true no parameter people is always present
includeQuestionIds=true no parameter responses[].question_id is always present (null when the response is not linked to a question)
includeLastReviewed=true no parameter reviewed_at is always present

v2 takes no query parameters on this call.

Response

v1 returns three fields while the audit is not complete:

{
  "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "status": "in_progress",
  "timestamp": "2026-09-24T02:34:54.489Z"
}

v1 returns the full audit once status is complete. This response comes from a request with all four include* switches set to true:

{
  "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "status": "complete",
  "score": 0.6666666666666666,
  "flags": ["Failed", "Missing Signature"],
  "reviewed_by": "",
  "set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
  "timestamp": "2026-09-24T02:34:54.489Z",
  "metadata": {},
  "responses": [
    {
      "response_id": "e1d2c3b4-a596-4877-9867-5a4b3c2d1e0f",
      "query": "Is the session note signed by the rendering provider?",
      "answer": "No",
      "context": "The signature block at the end of the note is empty.",
      "question_id": "7d6c5b4a-3928-4170-a695-847362514f3e"
    }
    /* ... one entry per question */
  ],
  "documents": ["0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"],
  "display_info": [
    {
      "order": 0,
      "key": "provider",
      "title": "Provider",
      "value": "Jordan Rivera",
      "format": "text",
      "filter_type": "text"
    }
  ],
  "people": [
    {
      "order": 0,
      "key": "client",
      "title": "Client",
      "value": "Sample Client",
      "format": "text",
      "filter_type": "text",
      "person_id": null
    },
    {
      "order": 1,
      "key": "provider",
      "title": "Provider",
      "value": "Jordan Rivera",
      "format": "text",
      "filter_type": "text",
      "person_id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d"
    }
  ],
  "people_emails": [
    { "title": "Client", "name": "Sample Client", "email": null },
    {
      "title": "Provider",
      "name": "Jordan Rivera",
      "email": "jordan.rivera@clinic.example"
    }
  ],
  "last_reviewed": "N/A"
}

v2 returns the same 13 fields on every status:

{
  "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "object": "audit",
  "status": "complete",
  "score": 0.6666666666666666,
  "set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
  "flags": ["Failed", "Missing Signature"],
  "created_at": "2026-09-24T02:34:54.489Z",
  "updated_at": "2026-09-24T02:37:12.105Z",
  "reviewed_at": null,
  "display_info": [
    {
      "key": "provider",
      "title": "Provider",
      "value": "Jordan Rivera",
      "format": "text"
    },
    {
      "key": "date_of_service",
      "title": "Date of service",
      "value": "2026-09-24T02:34:54.489Z",
      "format": "date"
    }
  ],
  "documents": ["0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"],
  "people": [
    {
      "person_id": null,
      "role": "client",
      "role_title": "Client",
      "name": "Sample Client"
    },
    {
      "person_id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
      "role": "provider",
      "role_title": "Provider",
      "name": "Jordan Rivera"
    }
  ],
  "responses": [
    {
      "response_id": "e1d2c3b4-a596-4877-9867-5a4b3c2d1e0f",
      "question_id": "7d6c5b4a-3928-4170-a695-847362514f3e",
      "question": "Is the session note signed by the rendering provider?",
      "answer": "No",
      "evidence": "The signature block at the end of the note is empty.",
      "remediation": "Ask the rendering provider to sign the note.",
      "affirmation": null
    }
    /* ... one entry per question; the full sample is in Retrieve an audit by id */
  ]
}

Field by field

At the top level of the audit, the fields change as follows:

v1 field v2 field Notes
audit_id audit_id Unchanged
(none) object Always "audit"
status status Same values. v2 returns every field on every status; v1 returns only audit_id, status and timestamp until complete
score score A number from 0 to 1, or null. v1 sends 0 when unset. Use it only when status is complete
flags flags An array of strings; [] when there are none
set_id set_id Unchanged; fetch the question set with GET /v2/question_sets/{set_id}
timestamp created_at RFC 3339 in UTC. v2 created_at and v1 timestamp can differ, so do not match records on either
(none) updated_at The time the audit last changed; the updated_after / updated_before filters use it
last_reviewed (with includeLastReviewed) reviewed_at Always present. The time the audit was last marked reviewed in Brellium. It is null when the audit is not marked reviewed now (v1 "N/A") or was marked reviewed before November 26, 2024
reviewed_by no v2 equivalent
metadata no v2 equivalent v1 always sends {}
documents documents An array of document_id values; fetch a document with GET /v2/documents/{document_id}
display_info (with includeDisplayInfo) display_info Always present; each item changes, see below
people (with includePeople) people Always present; each item changes, see below
people_emails no v2 equivalent In v2, call GET /v2/email_recipients/{person_id} for each people[].person_id that is not null. A 404 NOT_FOUND means the person is not an email recipient (for example a client) or the recipient was deleted
responses responses Always present, [] until the audit has responses; each item changes, see below

The fields of each display_info item map to v2 as follows:

v1 field v2 field Notes
key key Unchanged
title title Unchanged
value value Always a string; dates and times are RFC 3339 in UTC; "" when empty
format format Unchanged
order no v2 equivalent v2 returns the items already sorted by it
filter_type no v2 equivalent

Each person in people changes as follows:

v1 field v2 field Notes
key role Renamed, such as client or provider
title role_title Renamed, such as Provider
value name Renamed; the name as it appears on the audit
person_id person_id The person's id, shared by the email recipient and user for that person, or null
order no v2 equivalent v2 returns the people already sorted by it
format, filter_type no v2 equivalent

Each response in responses changes as follows:

v1 field v2 field Notes
response_id response_id Unchanged
query question Renamed; the question text
answer answer "Yes", "No" or "N/A"; also "N/A" when no answer can be read. v1 can also send "-"
context evidence Renamed. v2 returns the full evidence text, line breaks included; v1 returns only its first line. "" when none
question_id (with includeQuestionIds) question_id Always present; null when the response is not linked to a question
external_key no v2 equivalent v1 sends it only for questions that have one
(none) remediation The question's remediation text; null or "" when the question has none
(none) affirmation The question's affirmation text; null or "" when the question has none

Behavior changes that break a direct port

  • Branch on status, not on missing fields. v1 code that treats "has a score" as "is complete" sees a score on every v2 audit. Test status === "complete". Before that, score is null or 0, flags is [] and responses can be empty or partial.
  • Some audits that v1 returns by id return 404 NOT_FOUND in v2. v1 returns any audit of your organization by id, test audits included. v2 never returns test audits.
  • An id that is not a UUID returns 400 VALIDATION_ERROR; v1 does not check the format.
  • For a few seconds after an audit is created or changed, such as on its completion, GET /v2/audits/{audit_id} can return 404 or the previous version.

For the full v2 body and how to read it, see Retrieve an audit by id.

List and filter audits

POST /v1/audits with a JSON filter body becomes GET /v2/audits with query parameters.

Request

- POST https://<your v1 host>/v1/audits
- Content-Type: application/json
-
- {
-   "status": ["complete"],
-   "set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
-   "lastUpdatedStartDate": "2026-09-24T00:00:00Z",
-   "lastUpdatedEndDate": "2026-09-25T00:00:00Z",
-   "verbose": "true",
-   "page": 1,
-   "pageSize": 1000
- }
+ GET https://api.brellium.com/v2/audits
+     ?status=complete&set_id=3c2b1a09-8f7e-4d6c-b5a4-938271605f4e
+     &updated_after=2026-09-24T00:00:00Z&updated_before=2026-09-25T00:00:00Z
+     &limit=100
  Authorization: Bearer $BRELLIUM_ACCESS_TOKEN

Each v1 body field maps to v2 as follows:

v1 body field v2 query parameter Notes
status (array) status One value, exact and case-sensitive. To match several statuses, make one call per value. Repeating status returns 400 VALIDATION_ERROR
set_id set_id Unchanged; must be a UUID
documents (array of document ids) documents Same name. Send up to 100 document_id values, repeated or separated by commas; it matches audits that cover any of them. See List a document's audits
(none) external_id Matches audits that cover a document with this external_id
flags (array) flags Repeat it, up to 50; it matches audits with any of them, as in v1. Exact and case-sensitive
auditStartDate created_after Inclusive. v2 filters on created_at; v1 filters on timestamp, which can differ from created_at
auditEndDate created_before Inclusive, on created_at; v1 filters on timestamp
lastUpdatedStartDate updated_after Inclusive, on updated_at
lastUpdatedEndDate updated_before Inclusive, on updated_at
sessionStartDate, sessionEndDate no v2 equivalent v1 filters on the audit's session date, and no v2 field carries that date. Contact Brellium support if you depend on it
scoreLowerBound, scoreUpperBound no v2 equivalent v1 takes percents (0 to 100). Filter in your own code, and keep the audits whose score * 100 is in your range
page, pageSize limit, starting_after, ending_before Cursor pagination; see Pagination
verbose no parameter v2 always returns score, flags, set_id, documents and responses
includeResponses no parameter v1 ignores it (verbose controls responses); v2 always returns responses
includeDisplayInfo no parameter display_info is always present
includePeople no parameter people is always present
includeQuestionIds no parameter responses[].question_id is always present
includeLastReviewed no parameter reviewed_at is always present
includeTestAudits no v2 equivalent v1 "true" returns only test audits. v2 never returns them, and no parameter changes that
(none) audit_id Repeat it to fetch specific audits; see Batch get by id
(none) include_total true adds total_count, the number of matches across all pages

Send dates as RFC 3339 timestamps in UTC ending in Z, such as 2026-09-24T00:00:00Z or 2026-09-24T00:00:00.000Z. A date alone (2026-09-24), a timestamp without Z and a timestamp with an offset (+02:00) each return 400 VALIDATION_ERROR. URL-encode spaces in a flag (Missing%20Signature).

Response

v1 returns a bare array. Which fields an item carries depends on verbose and the include* switches; this item comes from a request with verbose: "true":

[
  {
    "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "status": "complete",
    "score": 0.6666666666666666,
    "flags": ["Failed", "Missing Signature"],
    "reviewed_by": "",
    "set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
    "timestamp": "2026-09-24T02:34:54.489Z",
    "last_updated": "2026-09-24T02:37:12.105Z",
    "responses": [ /* as in the v1 get response */ ],
    "documents": ["0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"],
    "people_emails": [ /* as in the v1 get response */ ],
    "document_id": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"
  }
]

v2 returns the list envelope. Each item in data is the full audit, the same object GET /v2/audits/{audit_id} returns:

{
  "object": "list",
  "data": [
    {
      "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "object": "audit",
      "status": "complete",
      "score": 0.6666666666666666,
      "set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
      "flags": ["Failed", "Missing Signature"],
      "created_at": "2026-09-24T02:34:54.489Z",
      "updated_at": "2026-09-24T02:37:12.105Z",
      "reviewed_at": null,
      "display_info": [ /* as in the v2 get response */ ],
      "documents": ["0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"],
      "people": [ /* as in the v2 get response */ ],
      "responses": [ /* as in the v2 get response */ ]
    }
  ],
  "has_more": true
}

Field by field

The v2 list envelope wraps the page in these fields:

v1 v2 Notes
the top-level array data One page of audits, not all of them
(none) object Always "list"
(none) has_more true when more audits follow; send the last audit_id as starting_after
(none) total_count Present only with include_total=true

Each item has every field of the GET /v2/audits/{audit_id} response, mapped as in Retrieve an audit. A v1 list item differs from the v1 GET /v1/audits/:audit_id response in these fields:

v1 list field v2 field Notes
last_updated updated_at Renamed
document_id no v2 equivalent v1 sends the smallest document_id (sorted as text) among the audit's documents, or among those in your documents filter. It is not necessarily the first document. To get the same value, take the smallest id in documents, counting only the ids in your filter when you sent one
score, flags score, flags v1 sends them only with verbose and only on a complete audit; v2 always does
responses, people_emails responses v1 sends responses only with verbose and only on a complete audit, and people_emails only with verbose
set_id, documents, reviewed_by set_id, documents v1 sends them only with verbose; reviewed_by has no v2 equivalent
metadata no v2 equivalent The v1 list does not send it

Pagination

v1 skips (page - 1) * pageSize audits and returns up to pageSize (default 1,000, no maximum). v2 returns up to limit audits (1 to 100, default 20) and has no page number; limit=1000 returns 400 VALIDATION_ERROR. Send the last audit_id of each page as starting_after until has_more is false. At limit=100, 1,000 audits take ten requests.

To fetch the second page, replace the v1 call as shown, where a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d is the last audit_id of the first page:

- POST /v1/audits
- { "status": ["complete"], "page": 2, "pageSize": 100 }
+ GET /v2/audits?status=complete&limit=100
+     &starting_after=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d

ending_before with the first audit_id of a page returns the page before it. For the pagination loop in code, see Page through and filter lists. To poll for changes, use updated_after and updated_before windows that overlap, as in Keep audits in sync with updated_after.

Batch get by id

v1 has no id filter; in v2, repeat audit_id in the list call, or send the ids in one value separated by commas. An id that matches no audit in your organization is left out, with no error.

curl --get "https://api.brellium.com/v2/audits" \
  --data-urlencode "audit_id=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
  --data-urlencode "audit_id=b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" \
  --data-urlencode "limit=100" \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams();
params.append("audit_id", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
params.append("audit_id", "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e");
params.append("limit", "100");

const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
  headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
if (!res.ok) throw new Error(JSON.stringify((await res.json()).error));
console.log(await res.json());
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

const audits = await client.audits.list({
  audit_id: [
    "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
  ],
  limit: 100,
});
console.log(audits);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

audits = client.audits.list(
    audit_id=[
        "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
    ],
    limit=100,
)
print(audits.to_json())

The page size still applies, so send limit=100 or follow has_more. A query string over 2,048 bytes is rejected with a bare 403 and no JSON body, so about 44 audit_id values (about 43 documents values) fit in one request; split larger batches.

Behavior changes that break a direct port

  • The sort order changes. v1 sorts by timestamp, newest first, then by audit_id ascending; v2 sorts by created_at, newest first, then by audit_id descending.
  • v2 includes audits whose question set has type coding, which v1 leaves out. To drop them, fetch the question sets of type coding with GET /v2/question_sets?type=coding and skip the audits whose set_id is one of them.
  • includeTestAudits has no v2 equivalent. v1 "true" returns only test audits, and v2 never returns them. With the coding audits above, this means the v1 and v2 lists do not match one for one.
  • status takes one value. v1 takes an array of statuses; in v2, make one call per status.
  • Lists can take a few seconds to include a new or changed audit.

Update audit flags

POST /v1/update-audits-flags becomes PATCH /v2/audits/{audit_id} for one audit or POST /v2/audits/batch_update for up to 100.

A v2 flag update replaces the audit's flags, where v1 adds the flags you send to the current ones. v2 stores exactly the list you send, so {"flags": ["Reviewed"]} removes every other flag and {"flags": []} removes them all. To add a flag, fetch the audit, add the flag to its flags, then send the full list.

Request

For one audit, PATCH takes the full list:

- POST https://<your v1 host>/v1/update-audits-flags
- Content-Type: application/json
-
- { "audit_ids": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"], "flags": ["Reviewed"] }
+ PATCH https://api.brellium.com/v2/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
+ Content-Type: application/json
+
+ { "flags": ["Failed", "Missing Signature", "Reviewed"] }

For several audits, batch_update takes one item per audit, each with that audit's full list:

- POST https://<your v1 host>/v1/update-audits-flags
- {
-   "audit_ids": [
-     "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
-     "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
-   ],
-   "flags": ["Reviewed"]
- }
+ POST https://api.brellium.com/v2/audits/batch_update
+ {
+   "items": [
+     {
+       "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
+       "flags": ["Failed", "Missing Signature", "Reviewed"]
+     },
+     {
+       "audit_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
+       "flags": ["Reviewed"]
+     }
+   ]
+ }

v1 can also select audits by their display fields with filter. v2 has no such mode, so list the audits and then update them by audit_id.

- POST https://<your v1 host>/v1/update-audits-flags
- { "filter": { "provider": "Jordan Rivera" }, "flags": ["Reviewed"] }
+ GET https://api.brellium.com/v2/audits?limit=100
+   (then keep the audits whose display_info has key "provider"
+   and value "Jordan Rivera")
+ POST https://api.brellium.com/v2/audits/batch_update
+   (items: each kept audit_id with its flags plus "Reviewed")
v1 body field v2 Notes
audit_ids (array) the {audit_id} path parameter of PATCH, or items[].audit_id of batch_update batch_update takes 1 to 100 items; v1 has no maximum. Each id must be a UUID
flags (array, added to each audit) flags (PATCH) or items[].flags (batch_update) The full list, which replaces the audit's flags. Up to 50 strings of 1 to 100 characters, case-sensitive, no duplicates; [] removes every flag. v1 has no limits and removes duplicates itself
filter (display field key and value pairs) no v2 equivalent v1 matches audits whose display_info contains every pair. In v2, list the audits, match display_info in your own code, then batch_update them

Response

v1 returns a message and the ids it changed:

{
  "message": "Successfully updated flags for 2 audits",
  "updated_audit_ids": [
    "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
  ]
}

PATCH returns the updated audit, the same object as GET /v2/audits/{audit_id}:

{
  "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "object": "audit",
  "status": "complete",
  "flags": ["Failed", "Missing Signature", "Reviewed"],
  "updated_at": "2026-10-21T15:00:00.000Z"
  /* ... the other audit keys, unchanged */
}

batch_update returns one result per item, in request order. Each result is the updated audit, or the item's audit_id with an error. The error is not_found_error / NOT_FOUND when your organization has no audit with that id, and api_error / INTERNAL_ERROR when the update could not be saved. has_more is always false.

{
  "object": "list",
  "data": [
    {
      "audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "object": "audit",
      "flags": ["Failed", "Missing Signature", "Reviewed"],
      "updated_at": "2026-10-21T15:00:00.000Z"
      /* ... the other audit keys */
    },
    {
      "audit_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
      "error": {
        "type": "not_found_error",
        "code": "NOT_FOUND",
        "message": "Audit not found"
      }
    }
  ],
  "has_more": false
}
v1 field v2 Notes
message no v2 equivalent
updated_audit_ids the audit_id of each data item without error v2 also returns each updated audit in full

Behavior changes that break a direct port

  • A flag update replaces the audit's flags; v1 adds to them. Fetch the audit, add your flag to its flags, then send the full list.
  • v2 rejects duplicate flags. A list with the same flag twice returns 400 VALIDATION_ERROR, with flags must be unique in details, and v2 writes nothing. v1 removes duplicates silently.
  • An unknown audit no longer blocks the rest of the update. In v1, one unknown id or another organization's id fails the whole request: no audit changes, and v1 returns a 500. In v2, PATCH returns 404 NOT_FOUND, and batch_update returns 200 with a NOT_FOUND result for that item and updates the others. v2 updates the items one at a time, in request order, and does not roll back the ones that succeed.
  • Validation covers the whole batch. One invalid item, such as a non-UUID id, a duplicate flag or a flag over 100 characters, returns 400 VALIDATION_ERROR, and v2 writes nothing.
  • Every v2 flag update sets a new updated_at, so your own updates appear in your next updated_after window. A v1 flag update does not change last_updated.
  • v1 updates any audit of your organization, test audits included. v2 returns 404 NOT_FOUND for a test audit, so you cannot update its flags.
  • GET /v2/audits/{audit_id} and lists can take a few seconds to show the new flags. The update response is the current audit, so use it instead of fetching the audit again.

For code that fetches, merges and updates the flags of one audit or a batch, see Update audit flags.

Error changes

Case v1 v2
Missing, invalid or expired token 401, plain text Unauthorized 401, authentication_error / UNAUTHORIZED
Unknown audit (retrieve) 404, {"msg": "audit not found"} 404, not_found_error / NOT_FOUND
audit_id that is not a UUID v1 does not check the format 400, invalid_request_error / VALIDATION_ERROR
Bad filter value, such as a date without Z or limit over 100 v1 does not validate it 400, invalid_request_error / VALIDATION_ERROR, with details per failing parameter
Body that is not valid JSON 400, {"message": "Invalid JSON format"} 400, invalid_request_error / HTTP_ERROR
Flag update without flags, or without audit_ids and filter 400, {"error": "flags array is required"} or {"error": "Either audit_ids or filter is required"} 400, invalid_request_error / VALIDATION_ERROR
Flag update body without Content-Type: application/json v1 also accepts a form-encoded body 400, invalid_request_error / INVALID_CONTENT_TYPE
Unknown audit, or another organization's audit, in a flag update 500 {"error": "Unexpected error occurred"}; no audit changes PATCH: 404, not_found_error / NOT_FOUND. batch_update: 200 with a per-item NOT_FOUND; v2 updates the other items
Too many requests (none) 429, rate_limit_error / RATE_LIMITED, with a Retry-After header
Server error 500 {"error": "Unexpected error occurred"} 500, api_error / INTERNAL_ERROR

Every v2 error body carries request_id, which equals the x-request-id response header; send it to Brellium support when you report a problem. Branch on error.type and error.code, which are stable; message can change. For every code and the retry rules, see Rate limits and errors.

Next