MetricSign
Start free
Best Practices9 min·

Azure Functions as Fabric REST API Middleware: Authentication, Polling, and the Errors Nobody Warns You About

Microsoft Fabric exposes a capable REST API for job scheduling, item management, and workspace automation. Azure Functions is the obvious glue layer. But token acquisition, long-running operation polling, and consumption plan timeouts create failure modes that surface only in production.

Lees dit artikel in het Nederlands →

Fabric has a REST API — your pipelines probably aren't using it yet

Most teams interacting with Microsoft Fabric programmatically are still clicking through the portal or using the Fabric notebooks UI. That's a problem once you need to trigger notebook runs from an external scheduler, create items across workspaces in a CI/CD flow, or cancel stalled jobs without logging into the browser.

The Fabric REST API (api.fabric.microsoft.com/v1) exposes operations that cover the full lifecycle: creating and deleting items, triggering on-demand job runs via the Job Scheduler endpoint, managing schedules (up to 20 per item), listing job instances, and bulk-importing item definitions. The API follows Azure conventions — bearer token auth, JSON payloads, and long-running operations that return 202 Accepted with a Location header you poll.

Azure Functions sits naturally in front of this API as a lightweight middleware layer. An HTTP-triggered function can accept a webhook from Azure DevOps, a timer-triggered function can kick off nightly Fabric notebook runs, and an Event Grid-triggered function can react to storage events by refreshing downstream Fabric artifacts. The pattern is straightforward: acquire a token, call the endpoint, handle the response.

But 'straightforward' breaks down in three places. Token acquisition requires the right scope and a service principal that Fabric actually recognizes. Long-running operations need polling logic that respects Retry-After headers. And consumption plan functions have a 5-minute default timeout — which is shorter than many Fabric operations take to complete. Each of these deserves its own section, because each produces a different class of silent failure.

Service principal tokens fail silently when the audience is wrong

The Fabric REST API requires an OAuth 2.0 bearer token with a specific audience. The scope you request from Azure AD (now Entra ID) must be https://api.fabric.microsoft.com/.default. Not the Power BI scope (https://analysis.windows.net/powerbi/api/.default), not the Azure management scope (https://management.azure.com/.default). Each of those will return a valid-looking JWT — and Fabric will reject it with a 401 or, worse, a 403 that tells you nothing useful.

In your Azure Function, token acquisition with Azure.Identity looks like this:

var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var token = await credential.GetTokenAsync(
    new TokenRequestContext(new[] { "https://api.fabric.microsoft.com/.default" }));

For Python functions using azure-identity:

from azure.identity import ClientSecretCredential
credential = ClientSecretCredential(tenant_id, client_id, client_secret)
token = credential.get_token("https://api.fabric.microsoft.com/.default")

Before any of this works, the service principal needs to be granted access in the Fabric admin portal. Navigate to Admin Portal → Tenant settings → Developer settings → Service principals can use Fabric APIs. Enable it and scope it to a security group that includes your app registration's service principal. Skip this step and every API call returns 401 — with no indication that it's a tenant configuration issue rather than a credential problem.

The app registration also needs the Fabric Service API permission (Application type) added in Entra ID → App registrations → API permissions. Without it, the token is issued but lacks the claims Fabric expects. The 403 response body will say InsufficientPrivileges, which at least points you in the right direction. Store client secrets in Azure Key Vault and reference them via the function app's Key Vault references in application settings. Hardcoded secrets in code or local.settings.json committed to repos are a common shortcut that becomes a security incident.

Azure Function to Fabric REST API: request lifecycle Azure Function triggered (timer/HTTP/event) Acquire service principal token with Fabric scope POST to Fabric REST API endpoint Response 200? Return result Response 202? Extract Location header Poll Get Operation State with Retry-After delay State = Succeeded: Get Operation Result State = Failed: Log error object and alert Response 401/403: Check scope and tenant settings Response 429: Exponential backoff with jitter
Azure Function to Fabric REST API: request lifecycle

Long-running operations need explicit polling — 202 is not 'done'

Several Fabric REST API operations return HTTP 202 Accepted instead of an immediate result. Triggering an on-demand job via POST /v1/workspaces/{workspaceId}/items/{itemId}/jobs/instances?jobType=RunNotebook is one. Bulk export and bulk import of item definitions are others. The 202 response includes a Location header pointing to the operation status endpoint and often a Retry-After header specifying how many seconds to wait before polling.

A naive Azure Function implementation fires the POST, gets 202, and returns success to the caller. The job may have failed 30 seconds later, but nobody checks. Building correct polling logic requires a loop:

var response = await httpClient.PostAsync(jobUrl, content);
if (response.StatusCode == HttpStatusCode.Accepted)
{
    var operationUrl = response.Headers.Location.ToString();
    var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(10);
    
    while (true)
    {
        await Task.Delay(retryAfter);
        var statusResponse = await httpClient.GetAsync(operationUrl);
        var status = JsonDocument.Parse(await statusResponse.Content.ReadAsStringAsync());
        var state = status.RootElement.GetProperty("status").GetString();
        
        if (state == "Succeeded") break;
        if (state == "Failed")
            throw new Exception(status.RootElement.GetProperty("error").ToString());
    }
}

The Get Operation State endpoint returns a status field with values like NotStarted, Running, Succeeded, and Failed. The Get Operation Result endpoint provides the actual output once the operation completes. Ignoring the distinction between state and result is a common mistake — polling the result endpoint before the operation finishes returns 200 with an empty or partial payload, not an error.

Respecting Retry-After matters beyond politeness. Fabric applies rate limiting (HTTP 429) at the tenant level. Polling more aggressively than the header suggests increases your chance of getting throttled, which then delays not just your function but every other API consumer in the tenant. When you receive a 429, the response includes its own Retry-After header — honor it or back off exponentially.

Consumption plan timeouts kill your polling loop at 5 minutes

Azure Functions on the Consumption plan have a default execution timeout of 5 minutes. The maximum configurable timeout on Consumption is 10 minutes (set functionTimeout in host.json to 00:10:00). A Fabric notebook run that takes 12 minutes to complete will outlast your function every time.

This is the most common production failure pattern for this architecture. The function triggers the Fabric job, begins polling, and gets killed by the host before the job finishes. The function logs show no error — just an abrupt end. The Fabric job itself might succeed or fail, but the function never captures the outcome.

Three approaches address this. First, use a Durable Functions orchestration instead of a single function. Durable Functions use checkpointing, so the orchestrator can await a timer for 30 seconds, poll, and repeat — without consuming compute during the wait. The total orchestration can run for days if needed:

[FunctionName("PollFabricJob")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var operationUrl = context.GetInput<string>();
    while (true)
    {
        var status = await context.CallActivityAsync<string>("CheckStatus", operationUrl);
        if (status == "Succeeded" || status == "Failed") return;
        await context.CreateTimer(
            context.CurrentUtcDateTime.AddSeconds(30), CancellationToken.None);
    }
}

Second, move to a Premium or Dedicated (App Service) plan where the timeout extends to 30 minutes or unlimited, respectively. This costs more but eliminates the architectural complexity of Durable Functions.

Third — and this is the pattern that scales best — decouple triggering from monitoring. Let the Azure Function trigger the Fabric job and immediately return the operation URL. A separate monitoring system checks whether the job completed within its expected window. If it didn't, you need to know. This is where MetricSign provides concrete value: it monitors Fabric pipeline execution end-to-end and alerts on runs that exceed expected duration or fail silently, with root cause context that includes the specific job instance ID and failure reason — details your function never captured because it timed out.

Error responses from Fabric follow a pattern — learn it once

Fabric REST API errors return a consistent JSON structure with an error object containing code and message fields. Some include an innerError with additional detail. The most frequent codes when calling from Azure Functions:

ItemNotFound (404) usually means the workspace ID or item ID is wrong, or the service principal lacks access to that specific workspace. Fabric doesn't distinguish between 'does not exist' and 'you cannot see it' — both return 404. This is a security measure, but it makes debugging harder. Double-check that the service principal is a workspace member with at least Contributor role.

Unauthorized (401) almost always traces back to the token scope issue described earlier, an expired token, or the tenant setting not enabling service principal access. If your function runs on a timer schedule, token caching can cause 401s when the cached token expires. Use GetTokenAsync with forceRefresh or let the Azure.Identity library handle token lifecycle — don't cache tokens manually.

TooManyRequests (429) is Fabric's rate limit response. The API enforces per-tenant throttling. If multiple Azure Functions, Power BI refreshes, and manual API calls all hit the same tenant simultaneously, 429s cascade. Implement exponential backoff with jitter, not fixed retry intervals. The Retry-After header value is in seconds.

InvalidItemType (400) occurs when you pass a jobType parameter that doesn't match the item type. Running jobType=RunNotebook against a Lakehouse item, for example. The Job Scheduler endpoint validates that the job type is compatible with the target item.

Log every API response — status code, headers, and body — in Application Insights. When a Fabric operation fails at 2 AM, the difference between 'something failed' and 'POST to /jobs/instances returned 429 with Retry-After: 60 at 02:14:33 UTC' is the difference between a 5-minute fix and an hour of guesswork.

Wire it together: a timer-triggered function that runs a Fabric notebook nightly

Here's the practical end-to-end pattern. A timer-triggered Azure Function acquires a service principal token, triggers a Fabric notebook run via the Job Scheduler API, and logs the operation URL for external monitoring.

[FunctionName("NightlyFabricNotebook")]
public static async Task Run(
    [TimerTrigger("0 0 2 * * *")] TimerInfo timer,
    ILogger log)
{
    var credential = new ClientSecretCredential(
        Environment.GetEnvironmentVariable("FABRIC_TENANT_ID"),
        Environment.GetEnvironmentVariable("FABRIC_CLIENT_ID"),
        Environment.GetEnvironmentVariable("FABRIC_CLIENT_SECRET"));
    
    var token = await credential.GetTokenAsync(
        new TokenRequestContext(new[] { "https://api.fabric.microsoft.com/.default" }));
    
    var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", token.Token);
    
    var workspaceId = Environment.GetEnvironmentVariable("FABRIC_WORKSPACE_ID");
    var itemId = Environment.GetEnvironmentVariable("FABRIC_NOTEBOOK_ID");
    var url = $"https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}" +
             $"/items/{itemId}/jobs/instances?jobType=RunNotebook";
    
    var response = await client.PostAsync(url, null);
    
    log.LogInformation("Fabric job trigger: {StatusCode}", response.StatusCode);
    log.LogInformation("Operation URL: {Location}", 
        response.Headers.Location?.ToString() ?? "none");
    
    if (response.StatusCode != HttpStatusCode.Accepted)
    {
        var body = await response.Content.ReadAsStringAsync();
        log.LogError("Fabric API error: {Body}", body);
    }
}

The function triggers at 2 AM UTC. It doesn't poll — it fires and logs. This keeps execution well within the 5-minute consumption plan timeout. Configuration values come from environment variables backed by Key Vault references: @Microsoft.KeyVault(VaultName=myvault;SecretName=fabric-client-secret) in the function app's application settings.

For the CRON expression, note that Azure Functions uses NCrontab with six fields (second minute hour day month day-of-week), not the five-field format from Unix cron. Writing 0 2 instead of 0 0 2 triggers at second 0, minute 2 — every hour, not once nightly. This is a common mistake that generates dozens of unexpected Fabric job runs before anyone notices.

Frequently asked questions

Can I use a managed identity instead of a service principal client secret for Fabric REST API calls?+
Yes. Azure Functions supports system-assigned and user-assigned managed identities. Use DefaultAzureCredential or ManagedIdentityCredential from the Azure.Identity library with the same Fabric scope (https://api.fabric.microsoft.com/.default). The managed identity's enterprise application must still be added to a security group enabled in Fabric's tenant settings for service principal API access. Managed identity eliminates secret rotation but requires the function app and Fabric tenant to be in the same Entra ID tenant.
What is the rate limit for the Fabric REST API?+
Microsoft applies per-tenant throttling but does not publish fixed rate limits. When throttled, the API returns HTTP 429 with a Retry-After header indicating how many seconds to wait. Throttling is shared across all API consumers in the tenant — Azure Functions, Power BI refreshes, manual REST calls, and Fabric pipelines all draw from the same pool. Implement exponential backoff with jitter rather than fixed retry intervals to avoid synchronized retry storms.
How do I trigger a Fabric pipeline (not notebook) from an Azure Function?+
Use the same Job Scheduler endpoint but change the jobType parameter. For Data Pipelines, use jobType=Pipeline. The endpoint is POST /v1/workspaces/{workspaceId}/items/{itemId}/jobs/instances?jobType=Pipeline. The item ID must reference a pipeline item, not a notebook or lakehouse. If the jobType doesn't match the item type, the API returns a 400 InvalidItemType error.

Related integrations

How we compare

Related articles