List segregation-of-duties violations
using System.Net.Http.Headers;var client = new HttpClient();var request = new HttpRequestMessage{ Method = HttpMethod.Get, RequestUri = new Uri("https://api.vendorica.com/v1/sod/violations?page=1&limit=100&sortBy=detectedAt&sortOrder=asc&subjectType=role"), Headers = { { "Authorization", "Bearer <token>" }, },};using (var response = await client.SendAsync(request)){ response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); Console.WriteLine(body);}package main
import ( "fmt" "net/http" "io")
func main() {
url := "https://api.vendorica.com/v1/sod/violations?page=1&limit=100&sortBy=detectedAt&sortOrder=asc&subjectType=role"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(res) fmt.Println(string(body))
}OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder() .url("https://api.vendorica.com/v1/sod/violations?page=1&limit=100&sortBy=detectedAt&sortOrder=asc&subjectType=role") .get() .addHeader("Authorization", "Bearer <token>") .build();
Response response = client.newCall(request).execute();const url = 'https://api.vendorica.com/v1/sod/violations?page=1&limit=100&sortBy=detectedAt&sortOrder=asc&subjectType=role';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use reqwest;
#[tokio::main]pub async fn main() { let url = "https://api.vendorica.com/v1/sod/violations";
let querystring = [ ("page", "1"), ("limit", "100"), ("sortBy", "detectedAt"), ("sortOrder", "asc"), ("subjectType", "role"), ];
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap());
let client = reqwest::Client::new(); let response = client.get(url) .query(&querystring) .headers(headers) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request GET \ --url 'https://api.vendorica.com/v1/sod/violations?page=1&limit=100&sortBy=detectedAt&sortOrder=asc&subjectType=role' \ --header 'Authorization: Bearer <token>'The violation register: every case where one holder was found with both halves of a duty pair the organization keeps separate. Filter with status, ruleId, subjectType and subjectId; page with page + limit. Findings are never deleted and a decision is terminal, so a consumer can poll on detectedAt and re-read a row to pick up its decision. Push delivery of the same finding is the sod.violation_detected webhook.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”Responses
Section titled “Responses”Success
object
object
The rule this finding cites, as shown in the app (SOD-1). Null only if the rule has since been removed — the finding outlives it.
What holds the conflict: role (one role concentrates both halves of a duty pair) or user (one person performed both halves on one record). Typed as a string rather than a closed enum because the set grows with the evaluation points; treat an unrecognised value as neither.
The role or person the finding is about, resolved at read time. Null when that subject no longer exists.
One half of what collided, as a tagged atom — permission:<name> for a role finding, activity:<key> for a four-eyes one.
What the evaluator saw, keyed by kind (role_grant or four_eyes). Carries the duty names, everything each duty matched, and enforcementAtDetection — the rung the rule was on WHEN THE FINDING WAS MADE. Correlate on that rather than on the rule’s rung today: a rule promoted since is a different statement about the same finding.
object
open awaits a decision. accepted and resolved are both terminal and neither returns to open — a conflict that recurs is filed as a NEW violation, because “it came back” and “it was never fixed” are different findings.
Why the decision was made. Mandatory when one is recorded, so this is non-null on every decided row.
object
Example
{ "success": true, "data": [ { "id": "26c9e168-79cd-49de-8c0d-f6e78cfba759", "ruleId": "ebb7b1b2-c22c-4810-8f65-c5e93a369417", "ruleDisplayId": "SOD-1", "subjectType": "role", "subjectId": "5bee1a50-09d3-4f16-8cd7-de0fc2d28b51", "subjectLabel": "string", "atomA": "permission:vendors:write", "atomB": "string", "evidence": {}, "detectedAt": "2026-08-19T09:41:12.004Z", "status": "open", "resolvedAt": "2026-08-19T09:41:12.004Z", "resolvedBy": "55571c38-9221-4f4e-87a2-3637275b5249", "resolutionNote": "string" } ], "timestamp": "2026-08-19T09:41:12.004Z", "requestId": "req_01J5X8ZC3K4T7Q9M2W6B0N1V4H", "message": "string", "pagination": { "page": 1, "limit": 1, "total": 1, "totalPages": 1 }}Validation error
object
Example
{ "success": false, "error": "Bad request", "code": "BAD_REQUEST", "timestamp": "2026-08-19T09:41:12.004Z", "requestId": "req_01J5X8ZC3K4T7Q9M2W6B0N1V4H"}Unauthorized
object
Example
{ "success": false, "error": "Unauthorized", "code": "UNAUTHORIZED", "timestamp": "2026-08-19T09:41:12.004Z", "requestId": "req_01J5X8ZC3K4T7Q9M2W6B0N1V4H"}Forbidden — missing scope
object
Example
{ "success": false, "error": "Forbidden", "code": "FORBIDDEN", "timestamp": "2026-08-19T09:41:12.004Z", "requestId": "req_01J5X8ZC3K4T7Q9M2W6B0N1V4H"}Not found
object
Example
{ "success": false, "error": "Resource not found", "code": "NOT_FOUND", "timestamp": "2026-08-19T09:41:12.004Z", "requestId": "req_01J5X8ZC3K4T7Q9M2W6B0N1V4H"}Rate limit exceeded
object
Example
{ "success": false, "error": "Too many requests", "code": "TOO_MANY_REQUESTS", "timestamp": "2026-08-19T09:41:12.004Z", "requestId": "req_01J5X8ZC3K4T7Q9M2W6B0N1V4H"}