Reference
API reference
Twelve endpoints, one of which is the reason the other eleven exist. Everything is idempotent except withdrawal, which is idempotent by nature.
https://api.iamsec.si/v4 Authorization: Bearer <session-token> Idempotency-Key: <uuid> # required on all writes except /revocations
The Idempotency-Key requirement exists because a half-finished negotiation retried twice is a policy nobody signed. Revocations are exempt from needing one: an already-revoked grant returning 204 is not a failure state, and the endpoint will never ask you to prove you meant it.
Endpoints
| Method | Path | Does | Latency p99 |
|---|---|---|---|
POST | /policies | Draft a proposal. Grants nothing | 38ms |
POST | /policies/{id}/narrow | Counterparty removes scope; cannot add | 31ms |
POST | /policies/{id}/affirm | Bind a factor to a policy version | 44ms |
GET | /policies/{id} | Fetch the document and its signature chain | 12ms |
POST | /sessions | Mint a window from a signed policy | 52ms |
GET | /sessions?state=active | Enumerate live windows | 9ms |
POST | /evaluate | The hot path. Every call re-resolves | 1.8ms |
POST | /checkins | Liveness. A gap past the ceiling revokes | 6ms |
POST | /limits | Declare a hard or soft limit | 0.9ms |
POST | /revocations | Everything, now. No reason field | 0.6ms |
GET | /audit | Your events, mutually redacted | 71ms |
POST | /aftercare/ack | Discharge an aftercare obligation | 23ms |
POST /v4/evaluate
Called on every protected operation, not once per session. It returns a decision, the statement that produced it, and the age of the affirmation behind it.
{
"session": "sess_01jd9",
"actor": "top/a-2210",
"subject": "bottom/b-4417",
"action": "contact.restraint",
"context": {
"monitor_present": false,
"sober_attested_at": 1763061120,
"elapsed_in_window_ms": 412000
}
}{
"decision": "deny",
"matched_statement": "negotiated/step-up",
"precedence": "capacity_gate",
"failed_gate": "monitor_present",
"overridable": false,
"appeal": null,
"affirmation_age_ms": 1820000,
"scope_max_age_ms": 5400000,
"next": "a monitor must hold before this action evaluates true. nothing else changes"
}POST /v4/revocations
The endpoint the whole product is arranged around. Its request body is nearly empty by intent, and the schema will not accept a reason if you try to send one.
{ "scope": "all" }HTTP/1.1 204 No Content X-Iamsec-Revoked: 7 X-Iamsec-Propagation-Ms: 0.6 X-Iamsec-Aftercare: opened, 6h, obligatory
| Property | Value |
|---|---|
| Reason field | Does not exist. Unknown fields are rejected, including reason, why and justification |
| Authentication | Required — and only the actor's own. No counterparty signature, no admin co-sign |
| Rate limits | Exempt, permanently, on every plan. See below |
| Idempotency key | Not required. Second call returns 204 as well |
| Blocking | Synchronous. The response lands after invalidation is committed, not queued |
| Undo | None. There is no DELETE /revocations. Resumption is a new policy |
Errors
Error semantics matter more than endpoint lists here, because most of these states are things a client should not retry.
| Code | Slug | Means | Retry? |
|---|---|---|---|
| 400 | malformed_policy | The document is not representable. Usually a wildcard | no |
| 401 | affirmation_expired | Your factor aged out. Re-affirm, do not re-ask | after affirming |
| 403 | hard_limit_immutable | Platform deny. Not a bug, not a plan limitation, not appealable | never |
| 409 | window_superseded | A newer signed version exists; this one is frozen | no |
| 410 | consent_withdrawn | The grant is gone and will not come back as this object | never |
| 422 | scope_ceiling_exceeded | You asked for a longer max age than the tier allows | after narrowing |
| 428 | affirmation_required | Step-up gate not met for a high or critical scope | after affirming |
| 429 | rate_limited | Ordinary backpressure. Never returned by /revocations | with jitter |
| 451 | negotiated_in_bad_faith | Reserved, and permanently so. See the note below | never |
Rate limits
Limits are per subject and generous, with one absolute exemption.
| Endpoint class | Limit | Exceeding it |
|---|---|---|
| /evaluate | 10,000/min/subject | 429 with jitter hint |
| /policies*, /sessions* | 600/min/subject | 429 |
| /audit | 60/min/subject | 429, then a conversation |
| /revocations | unlimited, forever | Not possible. A limiter here is a denial of service against consent itself |
Webhooks
{
"id": "evt_9f2c",
"type": "consent.withdrawn",
"occurred_at": "2026-11-13T21:41:07.412Z",
"data": {
"actor": "bottom/b-4417",
"breadth": "all",
"sessions_revoked": 7,
"propagation_ms": 0.6,
"aftercare_deadline": "2026-11-14T03:41:07Z"
}
}| Event | Fires when |
|---|---|
| policy.signed | Both parties have bound a current factor to the same version |
| session.opened | A window is minted and its token is live |
| checkin.missed | A heartbeat gap passed the scope ceiling — revocation has already happened |
| consent.withdrawn | Any party revoked. Delivery is best-effort; the revocation is not |
| hard_limit.attempted | Someone tried to grant or override a reserved resource |
| aftercare.sla_breached | An obligation went undischarged past its deadline. This one pages |
SDKs
| Runtime | Package | Notes |
|---|---|---|
| Node 20+ | @iamsec/sdk | Typed, edge-safe, no ambient credentials |
| Python 3.11+ | iamsec | Sync and async clients share one policy object |
| Go 1.23+ | github.com/iamsec/iamsec-go | The reference implementation of the evaluator |
| Rust | iamsec-core | WASM build for on-device check-ins with no network path |