Calling the API from .NET
Pass path ids as arguments and everything else in one Kwargs dictionary; its fields become the JSON body for POST/PUT/PATCH and query parameters otherwise, and failures throw typed errors such as NotFoundError.
Overview
Relevant for Developers writing calls against any endpoint
Every generated method takes three kinds of argument, in this order:
- Its path values, in path order. Each is an object: a number, a string or a JsonNode.
- One optional Kwargs options argument.
- A CancellationToken.
Kwargs follows the Python SDK's keyword convention exactly, so a Python example translates one to one. Responses are schema-free JsonNode values, because AlertOps response shapes vary by endpoint and sometimes by call. Text endpoints return string, and binary endpoints (exports, iCal) return byte[].
Path values and Kwargs
Relevant for Developers building requests
Kwargs is a Dictionary<string, object?> keyed by Python names. Every key that isn't reserved is a field.
| Key | Description |
| any field | Merged into the JSON body for POST, PUT, PATCH and body-carrying DELETEs. Sent as a query parameter for every other method. |
| body | An explicit body (any JSON-convertible value, such as a JsonArray). Fields merge into it when it is an object. A present body key set to null sends no body at all. |
| params | Extra query parameters, also for body methods. |
| files | Multipart parts: { ["file"] = new FilePart("log.txt", bytes, "text/plain") }. The body's fields become form fields. |
| headers | Extra request headers. |
Values can be CLR primitives, JsonNodes, dictionaries, sequences or anonymous objects. They are converted with AlertOpsJson.From.
await ao.Groups.ListAsync(new Kwargs { ["search"] = "Network", ["limit"] = 100 }); // GET ?search=…&limit=100
await ao.Groups.CreateAsync(new Kwargs { ["group_name"] = "Network Ops" }); // POST {"group_name": …}
await ao.Groups.Members.AddAsync(11, Kwargs.Body(new JsonArray(
new JsonObject { ["member_type"] = "User", ["member"] = "7", ["sequence"] = 1, ["roles"] = new JsonArray("Primary") })));
await ao.EscalationPolicies.UpdateBasicAsync(3, Kwargs.Body(wholeRecord)); // record may contain any key
Pass server records as body, not as fields
A record with a top-level body, params, files or headers key would be read as an option. Ingest payloads often have a body field. Whenever you send a record you read from the API, wrap it: Kwargs.Body(record).
Query values follow Python's rules:
- true and false become true/false.
- Sequences are comma-joined.
- null values are dropped.
- Numbers use Python's int/float formatting.
Path values are percent-encoded, so a name such as Email Official/2 is safe.
Pagination
Relevant for Developers reading long lists
Paginated endpoints have three methods: ListAsync (one page), ListAllAsync (every page collected) and ListIterAsync (an IAsyncEnumerable that fetches lazily).
var every = await ao.Groups.ListAllAsync(new Kwargs { ["search"] = "sdk" });
await foreach (var alert in ao.Alerts.ListIterAsync(new Kwargs { ["createdFrom"] = "2026-03-01", ["createdTo"] = "2026-03-08" }))
{
if ((string?)alert?["status"] == "Open") Console.WriteLine(alert!["alert_id"]);
}
| Style | Used by | Behaviour |
| page index | groups, users, policies, audit trail … | limit + offset, where offset is a page index (0, 1, 2), not a row offset. Stops on a short page. |
| cursor | v3 lists | pageSize + after, read from nextCursor / hasMore. |
| page number | notification history | pageNumber + pageSize. |
| metadata cursor | alerts | limit + next, read from metadata.next. |
| limit only | inbound log | One page. |
A list unwrapped from an envelope keeps the envelope, which holds paging metadata and counts. Read it with AlertOpsJson.RawOf(list).
Errors and retries
Relevant for Developers handling failures
Non-2xx answers throw a subclass of APIError (base AlertOpsException). Each carries:
- Status, Body, Method and Url.
- Op, the Python op id (such as Groups.get).
- OpId, the dotted id (groups.get).
- Messages, normalised from the AlertOps error shapes.
| Error | When |
| BadRequestError | 400 or 422 |
| AuthenticationError | 401 |
| PaymentRequiredError | 402, the feature is not in the plan |
| PermissionDeniedError | 403 |
| NotFoundError | 404 |
| ConflictError | 409 |
| RateLimitError | 429 after retries |
| ServerError | 5xx |
| TransportError | network failure after retries |
| ValueError / LookupError / TypeError | a helper rejected an argument, or a named thing (role, zone, type) does not exist |
Retries are automatic:
- Which calls:
- GET, PUT and DELETE retry on 429, 502, 503 and 504.
- POST retries only on 429 and on connect failures, so a create is never sent twice.
- Delay: BackoffFactor × 2^attempt (default 0.5 s, capped at 8 s, with ±25 % jitter).
- Retry-After: honoured when the server sends it, up to 32 s.
- Cancellation: the CancellationToken cancels the call and its pending retries.
Listeners and the escape hatch
Relevant for Developers adding logging or calling unmodelled paths
ao.AddListener(e => Console.WriteLine($"{e.OpId} {e.Method} {e.Url} {e.Status} {e.Elapsed:0.000}s"));
await ao.RequestAsync("GET", "/groups", new Kwargs { ["search"] = "Net" }, @base: "v2");
Troubleshooting
Relevant for Anyone debugging a call
TypeError: … does not take a request body
- You passed body to a GET. Use fields or params instead.
A PUT wiped fields on the record
- Many AlertOps PUTs replace the whole record. Use the read-merge-write helpers (ao.EscalationPolicies.UpdateAsync, ao.Schedules.UpdateFieldsAsync), or GET the record, change it and PUT it back with Kwargs.Body(record).
Page 2 repeats page 1
- You set offset to a row count. Offset is a page index. Use ListAllAsync or ListIterAsync instead.
Related Articles
- Getting Started with the AlertOps .NET SDK
- Helpers and the Semantic Layer in .NET
- Testing the .NET SDK
Need help?
Email support@alertops.com or contact your AlertOps representative.