Jev in Azure API Management can evaluate an inbound request before APIM sends it to a backend. The policy calls Jev with send-request, reads its structured answer, and applies your rules to accept, reject, or route the request.
In the video, I test a simple image-prompt check and a more complex filter with several questions. I would keep this in APIM when the job is routing or input validation. Once it needs substantial code, custom retries, or more data lookups, I would move that work to Azure Functions.
Download the PowerPoint: Jev inside Azure API Management. The slides cover the architecture, a routing example, and the trade-offs. The two policy code sections below are reserved for the simple and complex demo policies.

What Jev returns to an APIM policy
Jev is TypeSafe AI’s model for structured decisions. You send the content to evaluate as state, select a model, and define the questions. APIM can then branch on the returned values.
- Choice selects from options such as billing, technical support, or sales.
- Score evaluates content against an ordered rubric.
- Noul answers a yes/no question with a probability from 0 to 1.
The image-prompt checks use Noul. A value of 0.93 means the model assigns a 93% probability to “yes.” It does not mean the prompt scored 93% for image quality. Choice and Score have a separate confidence field; Noul does not. These examples evaluate the text of an image request, not an image file.
TypeSafe’s current model documentation lists $0.042 per million input tokens, with output tokens free, checked September 29, 2026. That is slightly more precise than the four-cent figure I use in the video. The low input price makes these small classification calls worth considering, but the gateway still waits for an external request.
How the request passes through APIM
The client calls the APIM endpoint. In the inbound policy, send-request posts the request content and questions to Jev and stores its response in a variable. The policy reads that response and decides whether to continue to the configured backend or return a rejection.
For the demo, I first test Jev directly in Postman, then put the same kind of evaluation inside APIM. To follow the pattern, you need a TypeSafe API key, an APIM API operation with a backend, and a client such as Postman. The slide deck also shows a Choice-based department-routing example. That is a separate example from the two image-prompt flows below.
Simple flow: Require an image prompt above 0.70
The first policy asks one question: is this request an image prompt? It forwards the request only when the answer is greater than 0.70. A value equal to 0.70 is rejected under the rule shown in the supplied diagram.

In the recorded APIM test, a request for a high-contrast frog image returns an image-prompt value of 0.93 and is accepted. “What is two and two” returns a low value and is rejected. The policy exposes diagnostic headers so I can see the value and threshold in Postman. The rejected call completes in under 200 ms in that run.
APIM policy 1: Simple image-prompt check
Simple Jev Call in APIM Policy
<policies>
<inbound>
<base />
<!-- Preserve the original request body -->
<set-variable name="originalBody" value="@(context.Request.Body.As<string>(preserveContent: true))" />
<!-- Ask Jev one yes/no question -->
<send-request mode="new" response-variable-name="jevResponse" timeout="20" ignore-error="false">
<set-url>https://api.typesafe.ai/v1/systemone</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>Bearer {{jev-api-key}}</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var originalBody =
(string)context.Variables["originalBody"];
var state = new JObject(
new JProperty("body", originalBody)
);
var questions = new JObject(
new JProperty(
"is_image_prompt",
new JObject(
new JProperty("type", "noul"),
new JProperty(
"instructions",
"Does this request appear to be asking to create, generate, draw, render, design, visualize, illustrate, edit, modify, enhance, transform, or otherwise produce an image, picture, photo, artwork, logo, diagram, visual, graphic, or other visual content?"
)
)
)
);
return new JObject(
new JProperty("state", state),
new JProperty("model", "jev-latest"),
new JProperty("questions", questions)
).ToString();
}</set-body>
</send-request>
<!-- Extract Jev score -->
<set-variable name="jevImagePrompt" value="@{
var response =
(IResponse)context.Variables["jevResponse"];
var json =
response.Body.As<JObject>(
preserveContent: true
);
return json["answers"]
["is_image_prompt"]
["noul"]
.Value<double>();
}" />
<!-- Reject unless score is > 0.70 -->
<choose>
<when condition="@(
(double)context.Variables["jevImagePrompt"] <= 0.70
)">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Jev-Image-Prompt" exists-action="override">
<value>@(
((double)context.Variables["jevImagePrompt"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Threshold" exists-action="override">
<value>0.700</value>
</set-header>
<set-header name="X-Jev-Decision" exists-action="override">
<value>REJECTED</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty(
"status",
"rejected"
),
new JProperty(
"message",
"Input Not Valid"
),
new JProperty(
"reason",
"Request was not classified as an image prompt with sufficient confidence."
),
new JProperty(
"imagePromptScore",
(double)context.Variables[
"jevImagePrompt"
]
),
new JProperty(
"threshold",
0.70
)
).ToString();
}</set-body>
</return-response>
</when>
</choose>
<!-- Score > 0.70, forward to backend -->
<set-backend-service base-url="https://httpbin.org/anything" />
</inbound>
<backend>
<forward-request />
</backend>
<outbound>
<base />
<set-header name="X-Jev-Image-Prompt" exists-action="override">
<value>@(
((double)context.Variables["jevImagePrompt"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Threshold" exists-action="override">
<value>0.700</value>
</set-header>
<set-header name="X-Jev-Decision" exists-action="override">
<value>ACCEPTED</value>
</set-header>
</outbound>
<on-error>
<return-response>
<set-status code="500" reason="APIM Policy Error" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty(
"status",
"error"
),
new JProperty(
"source",
context.LastError.Source
),
new JProperty(
"reason",
context.LastError.Reason
),
new JProperty(
"message",
context.LastError.Message
)
).ToString();
}</set-body>
</return-response>
</on-error>
</policies>Complex flow: Reject selected content before forwarding
The second example asks several questions in one Jev call. It checks whether the input relates to Pepsi, whether it is spam, malware, risky, or harmful, and whether it looks like an image prompt. APIM applies the returned answers in the order shown below; the diagram does not imply a separate network call for each box.

- Reject when the Pepsi-related value is 0.50 or higher.
- Otherwise, reject when any spam, malware, risky, or harmful value is 0.50 or higher.
- Otherwise, accept when the image-prompt value is 0.50 or higher.
- If the image-prompt value is below 0.50, also accept.
That final accept branch means this flow does not require an image prompt after the rejection checks pass. If your backend should receive only image prompts, change that final branch when you add your policy. A lower image-prompt value by itself does not reject the request in this example.
In the video, a scene containing someone eating Cracker Jacks returns an image-prompt value of 0.91 and a Pepsi-related value of 0.77. APIM rejects it on the Pepsi check. A later “what is two and two in a photo” request returns 0.45 for image intent and 0.55 for spam. That rejection comes from the spam check.

The Pepsi-related rejection takes about 423 ms in the captured response. The later spam rejection completes in under 200 ms. These are individual demo observations, not a latency guarantee or a controlled comparison. Test with your own payloads and APIM deployment before setting a request-time budget.
APIM policy 2: Complex content filter
Multi-Step Jev APIM Policy
<policies>
<inbound>
<base />
<!-- Preserve the original request body so it can still be forwarded -->
<set-variable name="originalBody" value="@(context.Request.Body.As<string>(preserveContent: true))" />
<!-- Call Jev -->
<send-request mode="new" response-variable-name="jevResponse" timeout="20" ignore-error="false">
<set-url>https://api.typesafe.ai/v1/systemone</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>Bearer {{jev-api-key}}</value>
</set-header>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var originalBody =
(string)context.Variables["originalBody"];
var state = new JObject(
new JProperty(
"method",
context.Request.Method
),
new JProperty(
"path",
context.Request.Url.Path
),
new JProperty(
"contentType",
context.Request.Headers.GetValueOrDefault(
"Content-Type",
""
)
),
new JProperty(
"body",
originalBody
)
);
var questions = new JObject(
/*
* IMAGE PROMPT CHECK
*
* If this is >= 0.50, the request is allowed even if
* Spam / Malware / Risky / Harmful are elevated.
*
* Pepsi remains an unconditional rejection rule.
*/
new JProperty(
"is_image_prompt",
new JObject(
new JProperty(
"type",
"noul"
),
new JProperty(
"instructions",
"Does this request appear to be asking to create, generate, draw, render, design, visualize, illustrate, edit, modify, enhance, transform, or otherwise produce an image, picture, photo, artwork, logo, diagram, visual, graphic, or other visual content? Interpret this broadly. If the request could reasonably be considered an image generation or image editing request, answer yes."
)
)
),
/*
* PEPSI / PEPSICO CHECK
*
* This is always enforced even for image prompts.
*/
new JProperty(
"related_to_pepsi",
new JObject(
new JProperty(
"type",
"noul"
),
new JProperty(
"instructions",
"Is any part of this request related to Pepsi, PepsiCo, or any PepsiCo-related company, subsidiary, affiliate, division, brand, product, service, employee, customer, supplier, partner, or business operation? This includes direct references as well as clear indirect references to PepsiCo-owned or affiliated brands or companies."
)
)
),
/*
* GENERAL RISK CHECKS
*
* These are enforced only when this is NOT
* classified as an image prompt.
*/
new JProperty(
"is_spam",
new JObject(
new JProperty(
"type",
"noul"
),
new JProperty(
"instructions",
"Is this request spam, unsolicited bulk content, deceptive advertising, or meaningless repetitive content?"
)
)
),
new JProperty(
"contains_malware",
new JObject(
new JProperty(
"type",
"noul"
),
new JProperty(
"instructions",
"Does this request contain or attempt to deliver malware, malicious code, exploit payloads, credential-stealing content, or instructions intended to compromise a computer system?"
)
)
),
new JProperty(
"contains_risky_text",
new JObject(
new JProperty(
"type",
"noul"
),
new JProperty(
"instructions",
"Does this request contain risky or dangerous content such as threats, phishing, social engineering, credential theft, destructive instructions, data exfiltration attempts, or instructions likely to cause harm?"
)
)
),
new JProperty(
"otherwise_harmful",
new JObject(
new JProperty(
"type",
"noul"
),
new JProperty(
"instructions",
"Apart from spam, malware, or explicitly risky text, does this request contain content that is harmful, abusive, fraudulent, dangerous, or otherwise should not be forwarded to the backend?"
)
)
)
);
return new JObject(
new JProperty(
"state",
state
),
new JProperty(
"model",
"jev-latest"
),
new JProperty(
"questions",
questions
)
).ToString();
}</set-body>
</send-request>
<!-- Extract complete Jev response -->
<set-variable name="jevJson" value="@{
var response =
(IResponse)context.Variables["jevResponse"];
return response.Body.As<JObject>(
preserveContent: true
);
}" />
<!-- Extract Image Prompt score -->
<set-variable name="jevImagePrompt" value="@{
var json =
(JObject)context.Variables["jevJson"];
return json["answers"]
["is_image_prompt"]
["noul"]
.Value<double>();
}" />
<!-- Extract Pepsi score -->
<set-variable name="jevPepsi" value="@{
var json =
(JObject)context.Variables["jevJson"];
return json["answers"]
["related_to_pepsi"]
["noul"]
.Value<double>();
}" />
<!-- Extract Spam score -->
<set-variable name="jevSpam" value="@{
var json =
(JObject)context.Variables["jevJson"];
return json["answers"]
["is_spam"]
["noul"]
.Value<double>();
}" />
<!-- Extract Malware score -->
<set-variable name="jevMalware" value="@{
var json =
(JObject)context.Variables["jevJson"];
return json["answers"]
["contains_malware"]
["noul"]
.Value<double>();
}" />
<!-- Extract Risky Text score -->
<set-variable name="jevRisky" value="@{
var json =
(JObject)context.Variables["jevJson"];
return json["answers"]
["contains_risky_text"]
["noul"]
.Value<double>();
}" />
<!-- Extract Harmful score -->
<set-variable name="jevHarmful" value="@{
var json =
(JObject)context.Variables["jevJson"];
return json["answers"]
["otherwise_harmful"]
["noul"]
.Value<double>();
}" />
<!--
Build rejection reasons.
Rules:
1. Pepsi >= 0.50
ALWAYS REJECT
2. Image Prompt >= 0.50
ALLOW, unless Pepsi triggered
3. If NOT an Image Prompt:
Reject if Spam, Malware, Risky or Harmful >= 0.50
-->
<set-variable name="rejectionReason" value="@{
double threshold = 0.50;
double imagePrompt =
(double)context.Variables["jevImagePrompt"];
double pepsi =
(double)context.Variables["jevPepsi"];
double spam =
(double)context.Variables["jevSpam"];
double malware =
(double)context.Variables["jevMalware"];
double risky =
(double)context.Variables["jevRisky"];
double harmful =
(double)context.Variables["jevHarmful"];
var reasons = new List<string>();
/*
* Pepsi is always a blocking condition.
*/
if (pepsi >= threshold)
{
reasons.Add(
"Pepsi or PepsiCo related content (" +
pepsi.ToString("0.000") +
")"
);
}
/*
* General safety checks only apply if this is
* NOT classified as an image prompt.
*/
if (imagePrompt < threshold)
{
if (spam >= threshold)
{
reasons.Add(
"Spam (" +
spam.ToString("0.000") +
")"
);
}
if (malware >= threshold)
{
reasons.Add(
"Malware or malicious content (" +
malware.ToString("0.000") +
")"
);
}
if (risky >= threshold)
{
reasons.Add(
"Risky or dangerous content (" +
risky.ToString("0.000") +
")"
);
}
if (harmful >= threshold)
{
reasons.Add(
"Potentially harmful content (" +
harmful.ToString("0.000") +
")"
);
}
}
return string.Join(
"; ",
reasons
);
}" />
<!-- Build human-readable acceptance reason -->
<set-variable name="acceptReason" value="@{
double threshold = 0.50;
double imagePrompt =
(double)context.Variables["jevImagePrompt"];
if (imagePrompt >= threshold)
{
return "Image Prompt";
}
return "No risk factors above threshold";
}" />
<!--
Reject if any ACTIVE rejection rule was triggered.
-->
<choose>
<when condition="@(
!string.IsNullOrEmpty(
(string)context.Variables["rejectionReason"]
)
)">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Jev-Image-Prompt" exists-action="override">
<value>@(
((double)context.Variables["jevImagePrompt"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Pepsi" exists-action="override">
<value>@(
((double)context.Variables["jevPepsi"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Spam" exists-action="override">
<value>@(
((double)context.Variables["jevSpam"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Malware" exists-action="override">
<value>@(
((double)context.Variables["jevMalware"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Risky" exists-action="override">
<value>@(
((double)context.Variables["jevRisky"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Harmful" exists-action="override">
<value>@(
((double)context.Variables["jevHarmful"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Threshold" exists-action="override">
<value>0.500</value>
</set-header>
<set-header name="X-Jev-Decision" exists-action="override">
<value>REJECTED</value>
</set-header>
<set-header name="X-Jev-Reason" exists-action="override">
<value>@(
(string)context.Variables["rejectionReason"]
)</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty(
"status",
"rejected"
),
new JProperty(
"message",
"Input Not Valid"
),
new JProperty(
"reason",
(string)context.Variables[
"rejectionReason"
]
),
new JProperty(
"threshold",
0.50
),
new JProperty(
"scores",
new JObject(
new JProperty(
"imagePrompt",
(double)context.Variables[
"jevImagePrompt"
]
),
new JProperty(
"pepsiRelated",
(double)context.Variables[
"jevPepsi"
]
),
new JProperty(
"spam",
(double)context.Variables[
"jevSpam"
]
),
new JProperty(
"malware",
(double)context.Variables[
"jevMalware"
]
),
new JProperty(
"riskyText",
(double)context.Variables[
"jevRisky"
]
),
new JProperty(
"harmful",
(double)context.Variables[
"jevHarmful"
]
)
)
)
).ToString();
}</set-body>
</return-response>
</when>
</choose>
<!--
Only reached when request has been accepted.
This includes:
- Image prompt >= 0.50 AND Pepsi < 0.50
- Non-image request with all risk scores < 0.50
-->
<set-backend-service base-url="https://httpbin.org/anything" />
</inbound>
<backend>
<forward-request />
</backend>
<outbound>
<base />
<!--
Return Jev decision information for accepted requests.
-->
<set-header name="X-Jev-Image-Prompt" exists-action="override">
<value>@(
((double)context.Variables["jevImagePrompt"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Pepsi" exists-action="override">
<value>@(
((double)context.Variables["jevPepsi"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Spam" exists-action="override">
<value>@(
((double)context.Variables["jevSpam"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Malware" exists-action="override">
<value>@(
((double)context.Variables["jevMalware"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Risky" exists-action="override">
<value>@(
((double)context.Variables["jevRisky"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Harmful" exists-action="override">
<value>@(
((double)context.Variables["jevHarmful"])
.ToString("0.000")
)</value>
</set-header>
<set-header name="X-Jev-Threshold" exists-action="override">
<value>0.500</value>
</set-header>
<set-header name="X-Jev-Decision" exists-action="override">
<value>ACCEPTED</value>
</set-header>
<set-header name="X-Jev-Reason" exists-action="override">
<value>@(
(string)context.Variables["acceptReason"]
)</value>
</set-header>
</outbound>
<!-- Formatted APIM runtime error response -->
<on-error>
<return-response>
<set-status code="500" reason="APIM Policy Error" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty(
"status",
"error"
),
new JProperty(
"source",
context.LastError.Source
),
new JProperty(
"reason",
context.LastError.Reason
),
new JProperty(
"message",
context.LastError.Message
),
new JProperty(
"section",
context.LastError.Section
),
new JProperty(
"path",
context.LastError.Path
),
new JProperty(
"policyId",
context.LastError.PolicyId
)
).ToString();
}</set-body>
</return-response>
</on-error>
</policies>When I would move the logic to Azure Functions
I would keep a small request classification or validation step in APIM when the result directly controls routing or rejection. It uses the gateway already handling the request, and the rules stay close to that decision.
When the policy grows into multi-step logic, custom retries, SDK calls, or data lookups, Azure Functions gives that code a better home. Logic Apps Standard is another option when the decision belongs inside a longer workflow. The extra external call is part of every request’s latency either way, so measure the full path.
For the broader model overview, see Jev by TypeSafe: speed, cost, and three practical uses. My Azure Integration Services learning path covers the Microsoft integration tools around this pattern.
References
- TypeSafe introduction: typed questions and combining questions in one request.
- TypeSafe Noul documentation: yes/no probabilities and threshold behavior.
- TypeSafe models and pricing: input types and current token pricing.
- Microsoft APIM send-request policy reference: timeout, response variables, and error handling.