Destructive API Parameters: Validate Before Deletion
Destructive API parameters need more than valid JSON. Verify account ownership, immutable IDs, scope, freshness, approvals, retries, and audit evidence.

AI agents make destructive API calls too easily when a request looks syntactically correct. A valid JSON body, a familiar account name, and a successful lookup do not prove that the agent is about to delete the intended thing. Before any external request removes, revokes, disconnects, cancels, or overwrites data, validate identity, ownership, scope, and freshness against the service itself.
I have watched careful engineers read a request as "delete this test environment" while the API interpreted it as "delete every environment under this organization." The code often had no bug in the narrow sense. It accepted a loosely formed selector, resolved a name from the wrong account, or trusted data fetched earlier in the run. An autonomous agent makes those ordinary mistakes faster and with more confidence.
This is not a case for asking the model to be more cautious. Put a deterministic gate between the agent's proposed request and the credentialed request. The gate should reject ambiguity by default and make the human approve the actual target and effect.
A well formed request can still name the wrong object
A request becomes safe enough to send only after you establish four separate facts: the resource is the right type, its immutable identifier is the intended one, it belongs to the approved parent account, and the operation will affect the approved set. Teams routinely blend these facts into one lookup. That shortcut is where deletion work goes bad.
Consider a service that exposes both a display name and an ID:
{
"id": "env_7d3a",
"name": "staging",
"account_id": "acct_blue",
"state": "active"
}
An agent that searches for staging has found a candidate, not an authorization to delete it. Many accounts have a staging environment. Even within one account, names can be reused after an old object disappears. The only safe action request is one built with the returned immutable ID and checked against an expected parent ID.
The distinction matters most when an API supports a parent path such as /accounts/{account_id}/environments/{environment_id}. Check both segments. Do not infer ownership because the ID appeared in an earlier search response or because the agent put the same account name in its plan. The parent in the action URL is part of the authorization boundary.
Resource type deserves its own check. APIs often use a shared search endpoint or return mixed records. A result named staging might be an environment, a project, an access group, or a saved template. If the service has different delete endpoints, select the endpoint only after verifying the returned type. Do not build an endpoint by concatenating a model-produced type string.
Finally, define the expected effect before you inspect candidates. "Remove the old deployment" might mean delete a deployment record, stop a running job, revoke a token issued to it, or delete the whole environment. Those effects cannot share one generic delete operation. Make the requested verb and its object type explicit in the action contract.
Names help people, IDs control the request
Use an immutable ID to address a resource, but show the approving person enough human context to catch a bad choice. An approval card that says only env_7d3a invites rubber stamping. One that says only staging invites ambiguity. Put both in front of the operator, along with the parent account and the operation's consequence.
A useful target record looks like this:
{
"operation": "delete_environment",
"account": {"id": "acct_blue", "name": "Blue Team"},
"target": {"id": "env_7d3a", "name": "staging", "type": "environment"},
"expected_state": "active",
"effect": "permanently removes this environment and its managed resources"
}
The account display name helps an operator spot that the agent wandered into the wrong tenant. The resource name helps them recognize the object. The IDs make the request unambiguous. The stated effect stops a common approval failure: a person thinks they approved a reversible stop action when the provider will remove data.
Do not resolve a name by taking the first search result. Search endpoints frequently rank by relevance, return partial matches, or paginate. If a task gives an exact name, require exactly one candidate after filtering by the parent account and expected type. Zero candidates should fail. More than one candidate should fail. Asking the agent to choose one is not a fix, because the agent lacks evidence that distinguishes them.
Case handling also needs a service specific rule. Some providers treat names as case sensitive; others normalize them. Do not normalize names on your side and then assume the provider will act the same way. Use the provider's returned resource as the authority, and retain the exact returned label in the approval record.
Tags, labels, and descriptions are context, not identity. They change often and users write nearly anything into them. A tag like temporary=true can narrow a reviewed list, but it should not replace an account binding or an immutable resource ID.
Scope must be concrete before the agent can ask for approval
A destructive operation has a scope even when its request body contains only one ID. The scope includes the parent account, the selected resources, child resources the provider removes automatically, and any filter that expands the selection. Make that scope concrete before you ask anyone to approve it.
Single object deletion has a simple contract: one immutable ID, one expected parent, one resource type. Bulk deletion needs a different contract. It must produce a resolved set first, then obtain approval for that set or for a bounded summary that a human can inspect. Sending a selector straight to a destructive endpoint gives the external service the job of deciding scope after the approval happened.
Suppose an agent proposes this request:
{
"account_id": "acct_blue",
"filter": {"label": "cleanup-candidate"},
"delete": true
}
That body hides the only fact the operator needs: which resources match right now. Expand it through a read-only list call, reject pagination you did not fully inspect, and normalize the result into IDs. Then present a count and a short sample with names. If the set is larger than the approved limit, stop and require a new instruction.
Never let an omitted filter mean "all." In request schemas, distinguish a required empty list from an absent selector. Better still, prohibit destructive endpoints from accepting filters at all inside the agent action interface. Have the gateway accept only a list of resolved IDs for bulk work.
A practical shape is:
{
"operation": "delete_resources",
"account_id": "acct_blue",
"resource_type": "snapshot",
"resource_ids": ["snap_104", "snap_105"],
"selection_observed_at": "2025-03-08T14:32:11Z"
}
Reject an empty resource_ids array unless the workflow explicitly allows it. Reject duplicate IDs. Reject IDs from another account. Enforce a maximum count that fits the operation the person approved. A limit is not a substitute for review, but it prevents a malformed loop from turning a two resource cleanup into a thousand resource incident.
Cascades belong in scope too. If deleting a project deletes repositories, deploy keys, environments, or billing records, state that before approval. If the provider only exposes cascade details after a preflight call, retain that response and require the agent to present it. "Delete project" is too vague when the project has dependent objects.
Read the service twice when time can change the target
A preflight read verifies intent, but it does not freeze the object. The resource can change, move accounts, or disappear between validation and deletion. For sensitive operations, read the target immediately before the mutation and use the service's concurrency control when it provides one.
HTTP gives this pattern a standard mechanism. RFC 9110 defines conditional requests with If-Match: the server performs the requested method only if the current representation matches an entity tag supplied by the client. A GET may return an ETag, and a subsequent DELETE can carry that exact value.
GET /v1/accounts/acct_blue/environments/env_7d3a HTTP/1.1
Authorization: Bearer [injected credential]
HTTP/1.1 200 OK
ETag: "v42"
Content-Type: application/json
{"id":"env_7d3a","account_id":"acct_blue","name":"staging","state":"active"}
After comparing the body with the approved target, send:
DELETE /v1/accounts/acct_blue/environments/env_7d3a HTTP/1.1
If-Match: "v42"
Authorization: Bearer [injected credential]
If the service returns 412 Precondition Failed, treat that as a successful guardrail. Do not ask the agent to retry the delete with no condition. Fetch the resource again, compare it with the approved facts, and ask for fresh approval if any relevant fact changed. A version conflict is evidence that your old approval may no longer apply.
Some services use revision numbers, update timestamps, generation fields, or request tokens instead of HTTP ETags. Use the provider's documented mechanism. If it offers none, reduce the interval between final read and write, make the action serial, and accept that you cannot prove the target stayed unchanged. That limitation should affect whether you allow unattended deletion.
Do not confuse a successful GET with permission to mutate. The read credential may see more than the write credential can alter, and authorization can change independently of object state. The mutation response still decides whether the provider accepted the request.
Validation belongs at the credential boundary
Validation done only in an agent prompt or in generated code is advisory. The component that holds or injects the credential must enforce the checks, because that component is the last place that can prevent an outbound request.
This boundary should receive a structured action proposal, not a free form URL and arbitrary headers. A narrow action definition can require fields such as account ID, resource ID, method, expected type, and expected version. It can construct the outgoing path from validated segments and reject query parameters that broaden scope.
Do not accept a complete URL from an agent and then attempt to parse safety out of it. URL encoding, repeated query keys, alternate hostnames, and path normalization turn that into a parser contest. Accept typed fields, validate each against the provider contract, and construct the URL yourself. The same rule applies to request bodies. Generate a known body shape instead of passing through a blob whose fields you did not inspect.
A minimal gate can enforce this sequence:
- Confirm the requested operation exists in an allowlist and its method is destructive by design.
- Resolve each claimed target with a read call made under the same parent account.
- Compare returned ID, type, parent, and required state to the structured proposal.
- Obtain approval for the resolved effect, then recheck freshness and send the mutation.
- Record the result, including provider request ID when it returns one.
Keep the allowlist small. A generic escape hatch called raw_http defeats every check in this article because the agent can reintroduce arbitrary destinations, methods, and bodies. Engineers add such escape hatches when an endpoint is missing, then forget it exists until it bypasses the guardrails they thought they had.
Credentials should remain outside the agent context. The agent needs the result of a permitted action, not a bearer token it can copy into a curl command, a log, or a third party integration. Sallyport follows this shape for its HTTP actions: it holds the credential in its encrypted vault, executes the request itself, and returns the result to the agent.
DELETE does not mean the request is simple or reversible
HTTP method names do not tell you the full business effect. RFC 9110 says DELETE asks an origin server to remove the association between a target resource and its current functionality. The RFC deliberately does not promise that data disappears immediately, that related data survives, or that a retry is harmless in your particular API.
Provider documentation must define the actual effect. Some APIs mark an object for later removal. Some tombstone it. Some detach it from a parent. Others cascade into children. Read the endpoint's response codes and lifecycle notes before you classify an operation as low risk.
Do not send a body with DELETE unless the provider explicitly documents it. RFC 9110 says content received in a DELETE request has no generally defined semantics and can cause implementations to reject the request. A deletion API that relies on filters in a body may be valid for that provider, but it deserves extra testing through its documented client path. It should not become an excuse to pass free form selectors from an agent.
Retries need equal care. A network timeout creates an unknown outcome: the provider may have completed deletion after your client stopped waiting. Retrying immediately can generate misleading logs, trigger a second effect on a poorly designed endpoint, or delete a replacement resource if your retry resolves by name.
Handle an unknown result with an inspect first rule. Query the exact immutable ID under the exact parent. If the object no longer exists and the provider's deletion model supports that reading, record the operation as completed with an uncertain first response. If it still exists, inspect its state and provider request history when available before deciding whether to retry. Reuse an idempotency key for operations that support one, but do not invent idempotence where the provider does not offer it.
A 204 No Content only says the server accepted and completed the HTTP interaction as defined by that endpoint. It does not prove that every downstream cleanup has finished. If an agent's next action depends on deletion being complete, poll a documented operation status or resource state rather than treating the empty response body as a guarantee.
Approval should show consequences, not raw transport
Humans make better decisions when an approval describes the effect in ordinary terms and includes the identifiers needed for verification. Showing a method, path, and JSON body is useful for an API engineer, but it pushes too much parsing work onto the person meant to catch a bad target.
For a single resource, the approval should say what will change, name the parent account, show the resource name and ID, and mention irreversible or cascading effects. For a bulk operation, show the count, a bounded sample, the selection rule used to create the list, and the fact that the final action uses the frozen IDs rather than the rule.
Do not ask for one broad approval at the beginning of a long agent run and use it for every later delete. The target set changes as the agent discovers resources. Bind approval to a session and to the resolved action. If the agent process changes, the approval should not silently follow a new process that may have different code or instructions.
The opposite failure is approval fatigue. Asking someone to click for every harmless read teaches them to click without reading, then gives a dangerous delete the same visual weight. Keep reads noninteractive where appropriate, require session authorization for a newly running agent, and reserve per action confirmation for credentials or actions with destructive effect. A person who sees fewer prompts can examine the prompts that matter.
The approval record needs a clear expiry. The longer an agent waits after validation, the less the preflight means. If the action cannot run promptly, make it resolve and request approval again. This can feel strict during a cleanup job. It is less expensive than explaining why an approval from an hour ago applied to a resource that had since been recreated.
Audit evidence must reconstruct the decision without exposing secrets
A useful audit trail answers more than "did a request happen?" It should let you reconstruct what the agent proposed, what the service reported before the mutation, what a person approved, what the gateway sent, and what the service returned.
Capture the normalized fields, not just a raw request string. Record the action name, agent session identity, parent account ID, resource IDs, expected versions, selection timestamp, approved effect, approval time, outbound method and path, response status, and provider request ID. Store hashes or redacted forms of request material when it may contain sensitive values. An audit log that copies authorization headers has created a second credential store.
Keep the preflight response or an integrity protected digest of it. Without it, a later reviewer cannot tell whether the agent deleted the wrong object because validation failed, the object changed after validation, or the external service behaved differently than documented. The difference determines what you repair.
Tamper evidence matters when the same machine runs the agent and the action gateway. A mutable text log can be edited by the process that caused the incident. Sallyport projects its Sessions and Activity journals from an encrypted, hash chained audit log, and sp audit verify can check that chain offline without a vault key. That does not turn a bad approval into a good one, but it makes later alteration of the record harder to hide.
Test your logging on failed requests as well as successful ones. Rejections, expired approvals, version conflicts, and malformed IDs show whether the controls actually blocked work. A journal full of successes tells you very little about whether the gate would refuse a dangerous request.
Build destructive actions as narrow contracts
The safest API action is boringly narrow. It accepts one known operation, requires a known parent and object ID, performs a preflight check, and has a stated effect. General interfaces feel productive until an agent makes a request you did not expect and your only defense is hoping an operator notices a subtle parameter.
Start by inventorying actions that can delete, revoke, rotate, disable, overwrite, publish, or trigger charges. For each action, write down the immutable target fields, parent fields, allowed states, cascade behavior, freshness mechanism, retry behavior, and approval text. If you cannot state those facts, do not offer the action to an autonomous agent yet.
Then run deliberate bad inputs through the gate: a valid resource ID under the wrong account, a matching display name with two results, a stale ETag, an empty bulk list, an omitted filter, a target recreated under the same name, and a timeout after send. These are the inputs that reveal whether the boundary validates meaning or merely validates JSON.
Do not solve ambiguity by making the agent write a longer plan. Make ambiguity unrepresentable in the action contract. An agent can propose intent and gather evidence. The credential boundary should decide whether the evidence names one permitted effect, at this moment, under this account. That division gives you a system you can inspect when the request is routine and trust when it is not.
FAQ
Is format validation enough before an AI agent deletes a resource?
No. A plausible identifier only proves that the request has the right shape. Your preflight must confirm that the object exists, belongs to the intended account or project, has the expected name and type, and falls inside the approved scope.
How can I confirm an API resource ID belongs to the right account?
Ask the external service for the current representation first, then compare immutable IDs and the fields the operator approved. Do not let the agent treat a search result, display name, or an old cached response as proof of identity.
Should a deletion approval show resource names or IDs?
Use IDs for the action, but show humans both the ID and a recognizable label, such as the account name, project path, resource type, and current state. Names help catch intent errors; IDs prevent ambiguous matching.
Can I trust an API dry run for destructive operations?
They are safe only if the provider documents them clearly and they run the same authorization and validation path as the real request. A local simulation that merely prints what would happen is useful for debugging, but it does not prove the provider will accept the request or affect the expected objects.
What if the resource changes after an agent validates it?
Treat it as stale evidence. Re-read the target immediately before the mutation, include a version token such as an ETag when the API supports it, and fail if the object changed. You cannot remove every race, but you can make the dangerous window small and visible.
Why are empty filters dangerous in automated delete requests?
An empty list must stop the run unless the human explicitly approved an operation that can affect zero targets. Empty scope values are a classic path to broad deletion because many APIs or command wrappers interpret absence as "all".
Can an AI agent safely retry a failed DELETE request?
No. Retries after a timeout need operation-specific handling. Use an idempotency key when the provider supports one, inspect the object state before retrying, and never assume that an unread response means the provider did nothing.
What is the difference between deleting one resource and bulk deletion?
A request to delete the named database has a narrow, obvious target. A request that deletes every database matching a tag, everything in an account, or every object older than a date has a broader scope and needs an explicit count, sample, and separate approval.
What should an audit log capture for an AI-driven deletion?
Record the agent session, the normalized request, the preflight response used for comparison, the approval, the exact outbound method and path, the response, and a stable digest of sensitive material. Keep secrets out of the journal while preserving enough evidence to reconstruct the decision.
Do I need a policy engine to control destructive API calls from agents?
Good guardrails do not need a general purpose rules language. Fixed checks for account binding, exact resource identity, allowed scope, freshness, and explicit approval handle the failures that cause most damage and remain understandable during an incident.