With your AI gateway
Keep your model gateway for model access and spend, and put Deliverd in front of the tool calls and API requests that need a rule or a person.
An AI platform team usually runs a model gateway: one address for every model, keys kept in one place, spend and rate limits per team. Deliverd does not replace it. The gateway sees the request to the model; Deliverd sees what the model asked an agent to do, and decides whether a rule settles it or a person has to. They sit at different points, so they work together whichever gateway you use.
| Your model gateway | Deliverd | |
|---|---|---|
| Sits in front of | The model | The tool call or API request an agent makes |
| Decides | Which model, which key, how much may be spent | Whether the action happens, and who agrees to it |
| Records | Tokens, latency, cost | Who asked, under which rule, who answered, what ran |
| A person is involved | Never | When no rule settles it, or a rule says so |
MCP servers and HTTP APIs: the hosted gateway
The most direct way to put Deliverd on a platform is in front of the tools themselves. Add an MCP server or an HTTP API under Organisation → Integrations & webhooks → MCP and HTTP gateways, give the agent the gateway's address and its own agent key, and keep the server's credential in the gateway alone. A call that changes something becomes a gate; a refusal never reaches the server. It does not matter which model or model gateway the agent uses, because the check happens when the agent calls the tool.
- Allowed: the call is forwarded with the signed decision token, and how it ended is recorded.
- A person must decide: the agent gets the approval link; the same call made again goes through once they say yes.
- Refused, or Deliverd cannot decide: the server never sees the call.
Apps on the AI SDK, with Vercel AI Gateway or any provider
The AI SDK has its own approval hook on generateText, streamText and ToolLoopAgent. deliverdToolApproval from @deliverd/sdk/adapters is one it accepts. The model can come from Vercel AI Gateway, LiteLLM or a provider directly; nothing about the model changes. A tool call when does not select runs as before; one it selects waits for a rule or a person, and a refusal hands the model the reason.
import { generateText } from "ai";
import { Deliverd } from "@deliverd/sdk";
import { deliverdToolApproval } from "@deliverd/sdk/adapters";
const result = await generateText({
model, // from your model gateway, as before
tools: { refund, sendEmail, lookupOrder },
toolApproval: deliverdToolApproval({
deliverd: new Deliverd(),
approvers: ["finance"],
when: (tool) => tool === "refund" || tool === "sendEmail",
}),
prompt,
});The same approach works for the Claude Agent SDK, the OpenAI Agents SDK, LangGraph and Mastra, each through its own approval hook. See SDKs.
APIs behind Azure API Management
If your APIs already sit behind Azure API Management, an inbound policy can ask Deliverd before an operation reaches the backend. The policy below asks the gate, answers 202 with the approval link while a person decides, refuses with 403, claims the use before forwarding so a replay is refused, and passes the signed yes on in Deliverd-Decision for the API to verify.
- 1
Register the action
An owner or administrator registers the action the operation performs, with an input schema that describes the request body. See The gate.
- 2
Create an agent key
Register an agent for the gateway under Agents → Agents and give its key
gates:writeandgates:execute. - 3
Add two named values
In API Management, add
deliverd-url(your Deliverd address, with no trailing slash) anddeliverd-agent-key(the key, as a secret). - 4
Add the policy to the operation
Paste it into the operation's inbound section and change
finance.refundto your action. Callers send aDeliverd-Request-Idheader, one value per unit of work and the same on a retry, so a retry re-reads its decision instead of raising a second one.
Inbound policy
<inbound>
<base />
<!-- One value per unit of work, the same on a retry -->
<set-variable name="workId" value="@(context.Request.Headers.GetValueOrDefault("Deliverd-Request-Id", ""))" />
<choose>
<when condition="@(string.IsNullOrEmpty((string)context.Variables["workId"]))">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-body>Send a Deliverd-Request-Id header: one value per unit of work, the same on a retry.</set-body>
</return-response>
</when>
</choose>
<!-- Ask the gate -->
<send-request mode="new" response-variable-name="gateResponse" timeout="20" ignore-error="true">
<set-url>{{deliverd-url}}/api/v1/gates</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>Bearer {{deliverd-agent-key}}</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@(new JObject(
new JProperty("actionId", "finance.refund"),
new JProperty("externalId", "apim:" + (string)context.Variables["workId"]),
new JProperty("input", context.Request.Body.As<JObject>(preserveContent: true))
).ToString())</set-body>
</send-request>
<set-variable name="gate" value="@(context.Variables.GetValueOrDefault<IResponse>("gateResponse")?.Body.As<JObject>())" />
<choose>
<!-- Deliverd could not be asked: fail closed -->
<when condition="@(context.Variables.GetValueOrDefault<JObject>("gate") == null)">
<return-response>
<set-status code="502" reason="Bad Gateway" />
<set-body>The policy check could not be reached, so the request was not sent.</set-body>
</return-response>
</when>
<!-- A person must decide: send the same request again once they have -->
<when condition="@((string)((JObject)context.Variables["gate"])["status"] == "pending")">
<return-response>
<set-status code="202" reason="Accepted" />
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
<set-body>@(new JObject(
new JProperty("status", "pending"),
new JProperty("approvalUrl", ((JObject)context.Variables["gate"])["approvalUrl"])
).ToString())</set-body>
</return-response>
</when>
<!-- Refused, or no longer usable -->
<when condition="@((bool?)((JObject)context.Variables["gate"])["canExecute"] != true)">
<return-response>
<set-status code="403" reason="Forbidden" />
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
<set-body>@(new JObject(
new JProperty("status", ((JObject)context.Variables["gate"])["status"]),
new JProperty("reason", ((JObject)context.Variables["gate"])["reason"])
).ToString())</set-body>
</return-response>
</when>
</choose>
<!-- Allowed: claim the use before acting, so a replay is refused -->
<send-request mode="new" response-variable-name="claimResponse" timeout="20" ignore-error="true">
<set-url>@("{{deliverd-url}}/api/v1/gates/" + (string)((JObject)context.Variables["gate"])["id"] + "/execution")</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>Bearer {{deliverd-agent-key}}</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{"phase": "started"}</set-body>
</send-request>
<choose>
<when condition="@(context.Variables.GetValueOrDefault<IResponse>("claimResponse") == null || ((IResponse)context.Variables["claimResponse"]).StatusCode >= 300)">
<return-response>
<set-status code="403" reason="Forbidden" />
<set-body>This permission has already been used.</set-body>
</return-response>
</when>
</choose>
<!-- Forward the signed yes; the API can verify it before it acts -->
<set-header name="Deliverd-Decision" exists-action="override">
<value>@((string)((JObject)context.Variables["gate"])["decisionToken"])</value>
</set-header>
</inbound>Gateways with their own hooks
Other gateways, LiteLLM's custom guardrails among them, can call out before or after a model request. Deliverd has no tested recipe for them yet. The question a gateway hook sees is the model request, not the tool call, so the recipes above are where to start: they check the action itself.
Try it on your own agent.
Free to start, no card. Sign up with Google, Microsoft or GitHub, and the first page after hands you an API key.