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 />
    </inbound>
    
    <backend>
        <!-- Send to Service Bus Queue -->
        <send-request mode="new" timeout="30" response-variable-name="sb-response" ignore-error="false">
            <set-url>@($"https://{{ServiceBusNamespace}}.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 requestBody = context.Request.Body.As<JObject>(preserveContent: true);
                var trackingId = Guid.NewGuid().ToString();
                
                // Store tracking ID for later retrieval
                context.Variables["trackingId"] = trackingId;
                
                return JsonConvert.SerializeObject(new {
                    trackingId = trackingId,
                    orderId = (string)requestBody["orderId"],
                    customerId = (string)requestBody["customerId"],
                    items = requestBody["items"],
                    submittedAt = DateTime.UtcNow.ToString("O")
                });
            }</set-body>
        </send-request>
    </backend>
    
    <outbound>
        <base />
        <return-response>
            <set-status code="202" reason="Accepted" />
            <set-body>@{
                var trackingId = (string)context.Variables["trackingId"];
                return JsonConvert.SerializeObject(new {
                    status = "Accepted",
                    trackingId = trackingId,
                    message = "Your request is being processed",
                    statusUrl = $"https://api.example.com/orders/{trackingId}/status"
                });
            }</set-body>
        </return-response>
    </outbound>
</policies>

Step 3: Using Named Values

Create SAS-Based Authentication

<!-- Store connection info in Named Values -->
<!-- ServiceBusSasKey: the shared access key value -->
<!-- ServiceBusSasKeyName: the key name (e.g., SendOnlyKey) -->

<policies>
    <inbound>
        <base />
    </inbound>
    
    <backend>
        <send-request mode="new" timeout="30" response-variable-name="sb-response">
            <set-url>@($"https://{{ServiceBusNamespace}}.servicebus.windows.net/orders/messages")</set-url>
            <set-method>POST</set-method>
            <set-header name="Authorization" exists-action="override">
                <value>@{
                    var sasKey = "{{ServiceBusSasKey}}";
                    var sasKeyName = "{{ServiceBusSasKeyName}}";
                    var resourceUri = "https://{{ServiceBusNamespace}}.servicebus.windows.net/orders/messages";
                    var expiry = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds();
                    
                    var stringToSign = $"{Uri.EscapeDataString(resourceUri)}\n{expiry}";
                    using var hmac = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(sasKey));
                    var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
                    
                    return $"SharedAccessSignature sr={Uri.EscapeDataString(resourceUri)}&sig={Uri.EscapeDataString(signature)}&se={expiry}&skn={sasKeyName}";
                }</value>
            </set-header>
            <set-header name="Content-Type" exists-action="override">application/json</set-header>
            <set-body>@(context.Request.Body.As<string>(preserveContent: true))</set-body>
        </send-request>
    </backend>
    
    <outbound>
        <base />
    </outbound>
</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://{{ServiceBusNamespace}}.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<JObject>(preserveContent: true);
                var trackingId = Guid.NewGuid().ToString();
                
                context.Variables["trackingId"] = trackingId;
                context.Variables["receivedAt"] = DateTime.UtcNow.ToString("O");
                
                return JsonConvert.SerializeObject(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 = (string)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>@{
                var trackingId = (string)context.Variables["trackingId"];
                return JsonConvert.SerializeObject(new {
                    status = "processing",
                    trackingId = trackingId,
                    estimatedCompletion = DateTime.UtcNow.AddMinutes(5),
                    statusUrl = $"/orders/{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-status code="200" reason="OK" />
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-body>@{
                var status = ((IResponse)context.Variables["status-response"]).Body.As<JObject>(preserveContent: true);
                return JsonConvert.SerializeObject(new {
                    trackingId = (string)status["trackingId"],
                    status = (string)status["status"],
                    progress = status["progress"],
                    result = status["result"],
                    completedAt = status["completedAt"]
                });
            }</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="@((bool)context.Variables.GetValueOrDefault("circuitOpen", false))">
            <return-response>
                <set-status code="503" reason="Service Unavailable" />
                <set-body>@(JsonConvert.SerializeObject(new { error = "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="@((int)context.Variables.GetValueOrDefault("failureCount", 0) + 1)" />
            
            <choose>
                <when condition="@((int)context.Variables.GetValueOrDefault("failureCount", 0) >= 5)">
                    <set-variable name="circuitOpen" value="@(true)" />
                    <set-variable name="circuitOpenedAt" value="@(DateTime.UtcNow)" />
                </when>
            </choose>
        </when>
    </choose>
</backend>

Benefits

BenefitDescription
Non-blockingClient receives response immediately
ReliableMessages persist until processed
ScalableWorker scales independently
MonitorableTrack processing status
ResilientFailed requests can retry

Azure Integration Hub - Advanced Level