Approvals and confirmations
Anything that is not a read is blocked before it reaches the OpusDNS API and requires your explicit approval. How you are asked depends on your client; the check on the server is the same either way.
What is gated
Every POST, PUT, PATCH and DELETE, with one exception: a POST that only
checks availability counts as a read, because asking whether a name is free has
no side effect. In today's API that exception covers exactly one operation,
POST /v1/availability/stream.
Reads — every GET, plus that one availability call — run without asking.
The risk wording
Each approval names the risk of the operation:
| Operation | Risk shown |
|---|---|
Register a domain (POST /v1/domains) |
can create costs, writes data |
| Renew a domain | can create costs, writes data |
Update a domain (PATCH /v1/domains/{domain_reference}) |
writes data, can delete or transfer resources |
Submit a batch (POST /v1/jobs) |
writes data |
Cancel a batch (DELETE /v1/jobs/{batch_id}) |
writes data, can delete or transfer resources |
| Pause a batch | writes data |
| Patch zone RRsets | writes data |
The risk wording is derived from the operation's own description, so it occasionally reads more alarming than the operation is. Update a domain is labelled "can delete or transfer resources" because setting a transfer lock is one of the things it does. The wording changes what the prompt says, never whether approval is required.
Which path your client takes
| Client | Path | What you see |
|---|---|---|
| Claude Code | A | An approval prompt in the terminal |
| Claude desktop and web | A | An approval prompt in the conversation |
| Cursor | B | The agent relays the confirmation_required payload and asks |
| VS Code | B | The agent relays the payload and asks |
| ChatGPT / OpenAI Responses API | B | The agent relays the payload and asks |
Path A needs two things at once: protocol revision 2026-07-28 and a
declared elicitation capability. A client missing either gets Path B, which
works everywhere. Both enforce the same gate server-side; they differ only in who
draws the prompt.
Path A — your client asks you directly
Clients on protocol revision 2026-07-28 that support elicitation get a
multi-round-trip request: the tool call terminates with an input request, and the
client renders its own approval prompt.
{
"resultType": "input_required",
"inputRequests": {
"approval": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Approve: Renew a domain (POST /v1/domains/{domain_reference}/renew)? Risk: can create costs, writes data.",
"requestedSchema": {
"type": "object",
"properties": {
"note": {
"type": "string",
"description": "Optional note recorded with the approval."
}
}
}
}
}
},
"requestState": "eyJ2IjoxLCJvcGVyYXRpb25JZCI6…"
}
The client asks you, then retries the same call with requestState echoed back
verbatim and your answer under inputResponses:
{
"name": "call_operation",
"arguments": {
"operationId": "renew_domain_v1_domains__domain_reference__renew_post",
"pathParams": { "domain_reference": "acme-labs.com" },
"body": {
"period": { "unit": "y", "value": 1 },
"current_expiry_date": "2026-11-04T00:00:00Z"
}
},
"requestState": "eyJ2IjoxLCJvcGVyYXRpb25JZCI6…",
"inputResponses": {
"approval": { "action": "accept", "content": { "note": "approved by kr" } }
}
}
Only "action": "accept" executes.
On this path the prompt is rendered by your client and the model never holds an unapproved token. It is the stronger of the two paths — prefer a client that supports it.
Path B — the confirmation payload
Every other client gets a payload the model has to relay to you:
{
"status": "confirmation_required",
"message": "Confirmation required before executing POST /v1/domains/{domain_reference}/renew (Renew a domain). Risk: can create costs, writes data. Ask the user for explicit approval, then retry the exact same action with this confirmation token.",
"confirmationToken": "eyJ2IjoxLCJvcGVyYXRpb25JZCI6…",
"expiresAt": "2026-08-25T14:35:12Z",
"operationId": "renew_domain_v1_domains__domain_reference__renew_post",
"safety": { "read": false, "write": true, "cost": true, "destructive": false }
}
Retrying with a confirmation token
Repeat the identical call with the token added as an argument:
{
"operationId": "renew_domain_v1_domains__domain_reference__renew_post",
"pathParams": { "domain_reference": "acme-labs.com" },
"body": {
"period": { "unit": "y", "value": 1 },
"current_expiry_date": "2026-11-04T00:00:00Z"
},
"confirmationToken": "eyJ2IjoxLCJvcGVyYXRpb25JZCI6…"
}
On this path your client has to actually show you the action and get your approval before retrying. The server can verify that the token belongs to this exact action, but it cannot verify that a human saw it. Choosing a client you trust with that is part of the security model — which is why Path A is preferable where it is available.
What the approval is bound to
There is one token type. On Path A it travels as requestState; on Path B as
confirmationToken. It is signed, and it binds:
| Bound to | Consequence |
|---|---|
| The operation | A token for one operation cannot approve another |
| A hash of the normalised action — path parameters, query, body, and the resolved list of domains for a bulk call | Change one byte of the request and the token stops matching |
| The signed-in user | Someone else's token is useless |
| The client and the granted scopes | A different client, or a re-login with different scopes, invalidates it |
| An expiry, about 5 minutes | It cannot be banked for later |
It is also single use, and it is consumed only after every other check has passed — so a retry that does not match never burns a token that is still valid.
Approval is enforced by the server, before any request reaches the OpusDNS API. The gate sits in the one code path every tool call flows through, so it cannot be bypassed by prompt engineering, by reaching for a different tool, or by a model that decides the instructions do not apply to it.
Declining, dismissing, and being asked again
You decline or dismiss the prompt. The call returns:
{
"status": "declined",
"operationId": "renew_domain_v1_domains__domain_reference__renew_post",
"message": "The user declined this action. Nothing was executed. Do not retry unless the user asks for it again."
}
Nothing was sent upstream, and the token was not consumed — it simply expires. You can still approve the same action before it does.
The client retried but lost your answer. The confirmation check fails and you are asked again, which is what the protocol prescribes for a missing response. Nothing is consumed.
The token expired, was already used, or does not match. You get a fresh challenge. See Troubleshooting.
Approving a bulk action safely
Three things to read in the prompt before you say yes:
-
The count. Does
matchedDomainsfrom the preview match what you expected? 412 and 4,127 look alike in a hurry. -
The selector echo. The prompt spells out the filters —
tld=["com"], expires_in_30_days=true. If it says "the entire portfolio (no filters)", the selector was empty. - The organization. If you are acting for a customer, the prompt names the sub-organization. If it does not, you are about to change your own portfolio.
Bulk approvals bind the resolved set
For a bulk call, the resolved list of domains is part of what the token binds. The list is sorted before it is hashed, so re-resolving the same selector reproduces the same approval — but if a domain was added, removed or renamed between the challenge and the retry, the hash no longer matches and you are asked again.
This is a feature. It means you approve "these 412 domains", not "a bulk update, roughly".
Related
- Bulk operations with Jobs
- Results and errors — the shapes an approval can return
- Roles & permissions — approval is not authorization; the API still decides what your account may do
- Troubleshooting

