Create an event
POST /v1/events decides an agent action before it runs. The SDKs call it from check().
decision is the outcome recorded on the event. It does not change when a review is resolved. finalDecision is the effective outcome: it equals decision when there is no review, stays REVIEW while a review is pending, becomes ALLOW after approval, and becomes BLOCK after denial or expiry. Resolving a review does not emit a new webhook. Poll GET /v1/events/{id}, which returns this same body.
Request
Section titled “Request”const decision = await vulnify.check({ agent: 'SalesBot', action: 'EXPORT_DATA', resource: 'Customer Database', destination: 'EXTERNAL_EMAIL', recordsAffected: 12000,});decision = vulnify.check( agent="SalesBot", action="EXPORT_DATA", resource="Customer Database", destination="EXTERNAL_EMAIL", records_affected=12000,)curl -X POST https://api.vulnify.io/v1/events \ -H "Authorization: Bearer $VULNIFY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "agent": "SalesBot", "action": "EXPORT_DATA", "resource": "Customer Database", "destination": "EXTERNAL_EMAIL", "recordsAffected": 12000 }'X-API-Key may replace the bearer header. See Authentication.
| Name | In | Required | Type | Description |
|---|---|---|---|---|
Idempotency-Key | header | no | string | Retries with the same key (per organization and endpoint, 24 hours) return the stored decision instead of deciding again. The replay always includes finalDecision, derived from the stored decision and the current review. |
agent or agentId is required unless the API key is bound to an agent. resource or resourceId is required.
| Field | Type | Required | Description |
|---|---|---|---|
agent | string (max length 100) | no | Agent name as registered in Vulnify. |
agentId | string (uuid) | no | Agent id. Use this or `agent`. |
resource | string (max length 100) | no | Resource name as registered in Vulnify. |
resourceId | string (uuid) | no | Resource id. Use this or `resource`. |
destination | INTERNAL | EXTERNAL_EMAIL | EXTERNAL_API | no | |
recordsAffected | integer (0–100000000) | no | |
content | string (max length 100000) | no | Optional text scanned for sensitive data. Scanned in memory and never stored. |
action | READ_DATA | WRITE_DATA | DELETE_DATA | EXPORT_DATA | SEND_EMAIL | yes |
Unknown JSON fields are rejected (additionalProperties: false). content is scanned in memory and never stored. Its max length is 100000 characters. The whole JSON body must also stay within 200 KB or the API returns 413. The Python client omits keys whose value is None.
Response
Section titled “Response”HTTP 200 is the decision. additionalProperties is true, so clients should ignore fields they do not know. quotaExceeded means the plan event quota is over the limit and the decision was still made. sandbox is true when the event was created with a TEST key. lgpdCategories is derived from dlpFindings.
{ "id": "00000000-0000-4000-8000-000000000000", "decision": "REVIEW", "finalDecision": "REVIEW", "evaluatedDecision": "REVIEW", "monitored": false, "riskLevel": "HIGH", "riskScore": 80, "reasons": ["external destination"], "policy": null, "dlpFindings": [], "lgpdCategories": [], "review": { "status": "PENDING", "expiresAt": null, "decidedAt": null, "note": null }, "quotaExceeded": false, "sandbox": false}After an approver accepts that review, a later GET still has "decision": "REVIEW" and "finalDecision": "ALLOW". Denial or expiry sets finalDecision to BLOCK.
An idempotent replay always includes finalDecision. The replay derives it from the stored decision and the current review, including when the stored response predates the field. The same Idempotency-Key applies for 24 hours per organization and endpoint, and the response sets Idempotent-Replay: true. See Authentication.
Decision: { id, decision, finalDecision, evaluatedDecision, monitored, riskLevel, riskScore, reasons, policy, dlpFindings, lgpdCategories, review, quotaExceeded, sandbox }. decision is the stored outcome and does not change when a review is resolved. finalDecision is the effective outcome (ALLOW after approval, BLOCK after denial or expiry, REVIEW while pending) and is always present, including an idempotent replay. quotaExceeded flags a plan overrun without blocking; sandbox is true for TEST keys.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes | |
decision | ALLOW | REVIEW | BLOCK | yes | Decision recorded on the event. It does not change when a review is resolved. |
finalDecision | ALLOW | REVIEW | BLOCK | yes | Effective outcome. Equals `decision` when there is no review. REVIEW while a review is pending. ALLOW after approval, BLOCK after denial or expiry. Always present, including an idempotent replay of a response stored before this field existed: the replay derives it from the stored decision and the current review. |
evaluatedDecision | ALLOW | REVIEW | BLOCK | yes | What full enforcement would have decided. Differs from `decision` in monitor mode. |
monitored | boolean | yes | |
riskLevel | LOW | MEDIUM | HIGH | CRITICAL | yes | |
riskScore | integer (0–100) | yes | |
reasons | array of string | yes | |
policy | object or null | yes | |
policy.id | string (uuid) | yes | |
policy.name | string | yes | |
dlpFindings | array of string | yes | Sensitive-data types found in `content`. |
lgpdCategories | array of IDENTIFICATION | CONTACT | LOCATION | FINANCIAL | HEALTH | COMPANY | CREDENTIALS | yes | LGPD categories derived from `dlpFindings`. |
review | object or null | yes | Null when the event has no human review. |
review.status | PENDING | APPROVED | DENIED | EXPIRED | yes | |
review.expiresAt | string or null (date-time) | yes | |
review.decidedAt | string or null (date-time) | yes | |
review.note | string or null | yes | |
quotaExceeded | boolean | yes | Plan quota is over the limit. The decision is still made. |
sandbox | boolean | yes | True when the event was created with a TEST key. |
| Header | Description |
|---|---|
X-Request-Id | Request id for support and log correlation (a caller-sent plain id is kept). |
x-api-version | Machine API contract (v1). |
x-vulnify-version | API build (SemVer, see /changelog). |
Idempotent-Replay | "true" when the body is a stored replay |
The SDKs add degraded: false on this path. degraded is not a field in the OpenAPI response. An outage never returns this body to the SDK. The SDK synthesizes a fallback locally. See Errors.
@vulnify/sdk 0.2.3 requires finalDecision on check() and getEvent(). The API always includes it, including an idempotent replay. vulnify 0.3.0 copies it to final_decision and still types that attribute as optional. Obey finalDecision. While decision stays REVIEW, approval is finalDecision ALLOW. See Node.js and Python.
Errors
Section titled “Errors”| Status | Meaning |
|---|---|
400 | Validation error (agent/agentId and resource/resourceId are required) |
401 | Missing, invalid, revoked or expired API key |
403 | Key bound to another agent, or caller IP not in the key allowlist |
404 | Unknown agent, resource or gateway route |
413 | JSON body larger than 200 KB |
429 | Rate limit exceeded |
A 413 (body over 200 KB) is a client error. The SDKs throw. Fail-open does not apply. See Errors.

