Azure API Management — APIM → Service Bus Integration
send-request Policy to Publish Messages, 202 Pattern
Introduction
Integrating Azure API Management with Azure Service Bus enables powerful asynchronous processing patterns. Instead of blocking HTTP requests while heavy processing completes, you can queue messages to Service Bus and return immediately with a 202 Accepted response.
This pattern is ideal for:
- Long-running operations — Process orders, generate reports, etc.
- High-throughput scenarios — Offload processing to background workers
- Resilient workflows — Messages persist until processed
- Decoupled architectures — Producer and consumer separated
Architecture Pattern
┌──────────┐ ┌────────────┐ ┌───────────────┐ ┌─────────────────┐
│ Client │───▶│ APIM │───▶│ Service Bus │───▶│ Worker/Func │
│ │ │ (Policy) │ │ Queue │ │ (Processor) │
└──────────┘ └────────────┘ └───────────────┘ └─────────────────┘
│ │
│ 202 Accepted │
│ { │
│ "status": "processing", │
│ "trackingId": "xxx" │
│ } │
▼ │
┌──────────┐ │
│ Poll │◀──────────────────────────┘
│ Status │ GET /status/{id}
└──────────┘
Step 1: Configure Managed Identity
Enable MI in APIM
# Enable system-assigned managed identity
az apim identity assign \
--name my-apim \
--resource-group my-rg
# Or use user-assigned identity
az apim identity assign \
--name my-apim \
--resource-group my-rg \
--user-assigned /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-identity
Grant Service Bus Permissions
# Get APIM's identity object ID
APIM_ID=$(az apim show --name my-apim --resource-group my-rg --query identity.principalId -o tsv)
# Grant Service Bus Sender role
az role assignment create \
--assignee $APIM_ID \
--scope /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/my-namespace \
--role "Azure Service Bus Data Sender"
Step 2: Create APIM Policy
Send to Service Bus
<policies>
<inbound>
<base />
<!-- Set backend to a placeholder - we'll use send-request -->
<set-backend-service backend-id="placeholder" />
</inbound>
<backend>
<!-- Send to Service Bus Queue -->
<send-request mode="new" timeout="30" response-variable-name="sb-response" ignore-error="false">
<set-url>@($"https://my-namespace.servicebus.windows.net/orders/messages")</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>@{
// Get token using Managed Identity
var tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://servicebus.azure.net/";
var tokenRequest = new HttpRequestMessage(HttpMethod.Get, tokenEndpoint);
tokenRequest.Headers.Add("Metadata", "true");
using var tokenClient = new HttpClient();
var tokenResponse = tokenClient.Send(tokenRequest);
var tokenJson = await tokenResponse.Content.ReadAsStringAsync();
var token = System.Text.Json.JsonSerializer.Deserialize<TokenResponse>(tokenJson);
return $"Bearer {token.access_token}";
}</value>
</set-header>
<set-header name="Content-Type" exists-action="override">application/json</set-header>
<set-body>@{
var requestBody = context.Request.Body.As<JsonElement>();
var trackingId = Guid.NewGuid().ToString();
// Store tracking ID for later retrieval
context.Variables["trackingId"] = trackingId;
return new {
trackingId = trackingId,
orderId = requestBody.GetProperty("orderId").GetString(),
customerId = requestBody.GetProperty("customerId").GetString(),
items = requestBody.GetProperty("items"),
submittedAt = DateTime.UtcNow.ToString("O")
};
}</set-body>
</send-request>
</backend>
<outbound>
<base />
<return-response>
<set-status code="202" reason="Accepted" />
<set-body>@{
return new {
status = "Accepted",
trackingId = context.Variables["trackingId"],
message = "Your request is being processed",
statusUrl = $"https://api.example.com/orders/{context.Variables["trackingId"]}/status"
};
}</set-body>
</return-response>
</outbound>
</policies>
Step 3: Using Named Values
Create SAS-Based Authentication
<!-- Store connection info in Named Values -->
<!-- ServiceBusSharedAccessKey: Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=xxx;SharedAccessKey=xxx -->
<policies>
<backend>
<send-request mode="new" timeout="30" response-variable-name="sb-response">
<set-url>@($"https://my-namespace.servicebus.windows.net/orders/messages")</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>@{
var key = "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={{ServiceBusKey}}";
var parts = key.Split(';');
var resource = "https://servicebus.azure.net/";
var expiry = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds();
var stringToSign = Uri.EscapeDataString($"https://my-namespace.servicebus.windows.net/orders/messages\n{expiry}");
var signature = ComputeHMACSHA256(stringToSign, parts[2].Split('=')[1]);
return $"SharedAccessSignature sr=https%3A%2F%2Fmy-namespace.servicebus.windows.net%2Forders&sig={Uri.EscapeDataString(signature)}&se={expiry}&skn={parts[1].Split('=')[1]}";
}</value>
</set-header>
<set-header name="Content-Type" exists-action="override">application/json</set-header>
<set-body>@{
return context.Request.Body.As<JsonElement>();
}</set-body>
</send-request>
</backend>
</policies>
Step 4: 202 Accepted Pattern
Complete Workflow
<policies>
<inbound>
<base />
<!-- Validate request -->
<validate-jwt header-name="Authorization">
<openid-config url="https://login.microsoftonline.com/tenant/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://my-api</audience>
</audiences>
</validate-jwt>
<!-- Rate limiting -->
<rate-limit-by-key calls="100" renewal-period="60"
counter-key="@(context.Subscription.Id)" />
</inbound>
<backend>
<!-- Send to Service Bus -->
<send-request mode="new" timeout="30" response-variable-name="sb-response">
<set-url>@($"https://my-namespace.servicebus.windows.net/orders/messages")</set-url>
<set-method>POST</set-method>
<authentication-managed-identity resource="https://servicebus.azure.net/" />
<set-header name="Content-Type" exists-action="override">application/json</set-header>
<set-body>@{
var body = context.Request.Body.As<JsonElement>();
var trackingId = Guid.NewGuid().ToString();
context.Variables["trackingId"] = trackingId;
context.Variables["receivedAt"] = DateTime.UtcNow;
return new {
trackingId = trackingId,
data = body,
metadata = new {
apiName = context.Api.Name,
operationName = context.Operation.Name,
subscriptionId = context.Subscription.Id,
receivedAt = DateTime.UtcNow
}
};
}</set-body>
</send-request>
</backend>
<outbound>
<base />
<!-- Return 202 Accepted -->
<return-response>
<set-status code="202" reason="Accepted" />
<set-header name="Location" exists-action="override">
<value>@{
var trackingId = context.Variables["trackingId"];
return $"/orders/{trackingId}/status";
}</value>
</set-header>
<set-header name="Retry-After" exists-action="override">
<value>30</value>
</set-header>
<set-body>@{
return new {
status = "processing",
trackingId = context.Variables["trackingId"],
estimatedCompletion = DateTime.UtcNow.AddMinutes(5),
statusUrl = $"/orders/{context.Variables["trackingId"]}/status"
};
}</set-body>
</return-response>
</outbound>
</policies>
Step 5: Status Check Endpoint
Status Query API
<policies>
<inbound>
<!-- Extract tracking ID from URL -->
<rewrite-uri template="/orders/status" />
</inbound>
<backend>
<!-- Query Redis/CosmosDB for status -->
<send-request mode="new" timeout="10" response-variable-name="status-response">
<set-url>@{
var trackingId = context.Request.MatchedParameters["trackingId"];
return $"https://my-redis.redis.azure.com/status/{trackingId}";
}</set-url>
<set-method>GET</set-method>
<authentication-managed-identity resource="https://redis.azure.net/" />
</send-request>
</backend>
<outbound>
<base />
<return-response>
<set-body>@{
var status = ((IResponse)context.Variables["status-response"]).Body.As<JsonElement>();
return new {
trackingId = status.GetProperty("trackingId"),
status = status.GetProperty("status"),
progress = status.GetProperty("progress"),
result = status.GetProperty("result"),
completedAt = status.TryGetProperty("completedAt", out var completed) ? completed : null
};
}</set-body>
</return-response>
</outbound>
</policies>
Step 6: Worker Function
Processing Messages
[FunctionName("OrderProcessor")]
public static async Task Run(
[ServiceBusTrigger("orders", Connection = "ServiceBusConnection")]
ServiceBusReceivedMessage message,
ServiceBusClient client,
ILogger logger)
{
var body = JsonSerializer.Deserialize<OrderMessage>(
Encoding.UTF8.GetString(message.Body));
logger.LogInformation("Processing order {TrackingId}", body.TrackingId);
try
{
// Update status to processing
await UpdateStatusAsync(body.TrackingId, "processing", 0);
// Process the order
var result = await ProcessOrderAsync(body.Data);
// Update status to completed
await UpdateStatusAsync(body.TrackingId, "completed", 100, result);
logger.LogInformation("Order {TrackingId} completed", body.TrackingId);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to process order {TrackingId}", body.TrackingId);
await UpdateStatusAsync(body.TrackingId, "failed", 0, new { error = ex.Message });
if (ex is RetryableException)
{
throw; // Will be retried
}
}
}
private async Task UpdateStatusAsync(
string trackingId,
string status,
int progress,
object result = null)
{
var redis = _redisClient.GetDatabase();
var key = $"order:status:{trackingId}";
var statusObj = new
{
trackingId,
status,
progress,
result,
updatedAt = DateTime.UtcNow
};
await redis.StringSetAsync(key, JsonSerializer.Serialize(statusObj));
await redis.KeyExpireAsync(key, TimeSpan.FromDays(7));
}
Error Handling
Circuit Breaker Pattern
<backend>
<choose>
<when condition="@(context.Variables.GetValue<bool>("circuitOpen"))">
<return-response>
<set-status code="503" />
<set-body>Service temporarily unavailable</set-body>
</return-response>
</when>
</choose>
<send-request mode="new" timeout="30" response-variable-name="sb-response">
<!-- ... -->
</send-request>
<choose>
<when condition="@(((IResponse)context.Variables["sb-response"]).StatusCode >= 500)">
<set-variable name="failureCount"
value="@(context.Variables.GetValue<int>("failureCount") + 1)" />
<choose>
<when condition="@(context.Variables.GetValue<int>("failureCount") >= 5)">
<set-variable name="circuitOpen" value="true" />
<set-variable name="circuitOpenedAt" value="@(DateTime.UtcNow)" />
</when>
</choose>
</when>
</choose>
</backend>
Benefits
| Benefit | Description |
|---|---|
| Non-blocking | Client receives response immediately |
| Reliable | Messages persist until processed |
| Scalable | Worker scales independently |
| Monitorable | Track processing status |
| Resilient | Failed requests can retry |
Azure Integration Hub - Advanced Level