HTTP Requests & APIs
Call any API from a flow with confidence. The premium HTTP action end to end — authentication (OAuth, Key Vault, Managed Identity, certificates, SAS), OData querying, file handling, inbound webhooks, async polling, throttling and $batch — plus a copy-paste cookbook for Graph, SharePoint, Dataverse, and Azure DevOps, and a request builder that scaffolds the action JSON with query params, auth presets, and a cURL equivalent.
{
"method": "GET",
"uri": "https://graph.microsoft.com/v1.0/users",
"queries": {
"$top": "5"
},
"headers": {
"Accept": "application/json"
}
}curl -X GET 'https://graph.microsoft.com/v1.0/users?%24top=5' \
-H 'Accept: application/json'The HTTP action
The HTTP action sends a raw request to any reachable endpoint. It is a premium connector, so every user who runs the flow needs a Power Automate premium license (or the flow runs in a Process/per-flow plan). For Microsoft 365 data, prefer the first-party connectors (Graph via Office 365 Groups, SharePoint, etc.) before reaching for raw HTTP.
| Field | Purpose | Example |
|---|---|---|
| Method | HTTP verb | GET, POST, PATCH, DELETE |
| URI | Full endpoint URL | https://api.example.com/v1/orders |
| Headers | Key/value pairs | Content-Type: application/json |
| Queries | URL query parameters | $filter, $top, api-version |
| Body | Request payload (JSON) | { "status": "open" } |
Premium + governance
Raw HTTP bypasses DLP-friendly connectors. Confirm your environment’s data-loss-prevention policy allows the HTTP connector before building on it, and store secrets in environment variables / Key Vault — never inline.
Authentication
| Type | How | Use when |
|---|---|---|
| API key | Add an Authorization or x-api-key header | Simple service tokens |
| Basic | Authentication = Basic, username + password | Legacy endpoints |
| Bearer (raw) | Authorization: Bearer <token> header | You already hold a token |
| Azure AD OAuth | Authentication = Active Directory OAuth (client credentials) | Graph / Azure APIs |
| Managed identity | Authentication = Managed Identity | Azure resources, no secret |
Client-credentials OAuth (app-only)
Authority: https://login.microsoftonline.com
Tenant: <tenant-id>
Audience: https://graph.microsoft.com
Client ID: <app-registration-client-id>
Credential: Secret
Secret: @{parameters('GraphClientSecret')} // from an environment variableAuth deep-dive: Key Vault, Managed Identity, certificates, SAS
Production auth means getting secrets out of the flow body entirely. Prefer a workload identity (Managed Identity) where the target accepts Azure AD tokens; fall back to a secret or certificate that lives in Azure Key Vault, never in an action.
Azure Key Vault
- Store the secret in Key Vault (e.g.
GraphClientSecret). - Grant the identity that resolves it Get on secrets — an access policy or the
Key Vault Secrets UserRBAC role. - Create a Dataverse environment variable of type *Secret* that points at the Key Vault secret. The value is resolved at runtime and is never stored in the solution.
- Reference it as
@{parameters('GraphClientSecret (secret)')}, or fetch it on demand with the Azure Key Vault → Get secret action.
Identity beats secrets
If the target supports Azure AD (Graph, Storage, Key Vault, Azure SQL), use Managed Identity instead of a stored secret — there is nothing to rotate and nothing to leak.
Managed Identity
Set Authentication = Managed Identity on the HTTP action and supply the Audience of the resource you are calling (e.g. https://graph.microsoft.com, https://vault.azure.net, https://<account>.blob.core.windows.net). Grant the identity an Azure RBAC role on the target (e.g. Key Vault Secrets User, Storage Blob Data Reader). The platform acquires the token — no secret in the flow.
Certificate (client) authentication
Authentication: Active Directory OAuth
Authority: https://login.microsoftonline.com
Tenant: <tenant-id>
Audience: https://graph.microsoft.com
Client ID: <app-registration-client-id>
Credential: Certificate
Pfx: @{parameters('AuthCertPfxBase64')} // base64 of the .pfx, from Key Vault
Password: @{parameters('AuthCertPassword')}SAS tokens (Azure Storage)
For Azure Storage, a Shared Access Signature is appended to the URL as query params — scoped to a resource and time-limited, with no Authorization header. Generate it server-side (or with the Storage connector) and store it in Key Vault rather than minting account keys in the flow.
A SAS in the URL leaks into run history
Query-string credentials (SAS, sig=, code=) are visible in the run’s action inputs. Turn on Secure inputs (Settings → Security) on any action whose URL carries a signature.
Calling Microsoft Graph
- Register an app in Entra ID, add the application Graph permissions you need (e.g.
User.Read.All), and grant admin consent. - Create a client secret and store it in an environment variable (or pull from Key Vault).
- In the HTTP action set Authentication = Active Directory OAuth with audience
https://graph.microsoft.com. - Call the endpoint and Parse JSON the response.
GET https://graph.microsoft.com/v1.0/users?$select=displayName,mail&$top=5
Accept: application/jsonParse JSON
Raw HTTP output is untyped. Add a Parse JSON action, click Generate from sample, paste a real response, and the designer infers a schema so downstream actions get typed dynamic content.
{
"value": [
{ "id": "1", "displayName": "Ada Lovelace", "mail": "ada@contoso.com" }
],
"@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=..."
}first(body('Parse_JSON')?['value'])?['displayName']Make fields nullable
APIs omit fields. In the generated schema, remove tightly-required properties or add "type": ["string","null"] so a missing value does not fail the Parse JSON action at runtime.
OData query deep-dive
Graph, SharePoint, and Dataverse all speak OData. Push filtering and shaping into the query string so the API returns only the rows and columns you need — faster, cheaper, and far less likely to be throttled than pulling everything and filtering in the flow.
| Operator | Meaning | Example |
|---|---|---|
| eq | Equals | $filter=state eq 'open' |
| ne | Not equals | $filter=state ne 'closed' |
| gt | Greater than | $filter=amount gt 1000 |
| ge | Greater than or equal | $filter=amount ge 1000 |
| lt | Less than | $filter=amount lt 50 |
| le | Less than or equal | $filter=amount le 50 |
| and | Both conditions | $filter=state eq 'open' and amount gt 1000 |
| or | Either condition | $filter=state eq 'open' or state eq 'pending' |
| not | Negation | $filter=not(state eq 'closed') |
| contains | Substring match | $filter=contains(subject,'invoice') |
| startswith | Prefix match | $filter=startswith(displayName,'A') |
| endswith | Suffix match (needs advanced query on Graph) | $filter=endswith(mail,'@contoso.com') |
| Option | Purpose | Example |
|---|---|---|
| $select | Return only these columns | $select=displayName,mail |
| $expand | Inline a related entity | $expand=manager($select=displayName) |
| $orderby | Sort (asc/desc) | $orderby=createdDateTime desc |
| $top | Page size (max varies by API) | $top=50 |
| $skip | Skip N records (where supported) | $skip=100 |
| $count | Include a total count | $count=true |
| $skiptoken | Opaque continuation cursor | returned inside @odata.nextLink |
GET https://graph.microsoft.com/v1.0/users
?$filter=accountEnabled eq true and startswith(displayName,'A')
&$select=displayName,mail,jobTitle
&$orderby=displayName
&$top=25Encoding & string literals
String literals go in single quotes; escape an embedded quote by doubling it (O''Brien). Spaces and reserved characters must be URL-encoded — the action’s Queries grid encodes for you, so prefer it over hand-building the URI.
Graph advanced queries
Some Graph filters (endsWith, $count on directory objects, $search) require *advanced query* mode: add the header ConsistencyLevel: eventual and $count=true. Dataverse uses logical names in $filter and supports Prefer: odata.include-annotations="*" to return formatted values.
Pagination
Many APIs (Graph included) return a page plus a continuation link. Two options: turn on the action’s built-in Pagination setting (Settings → Pagination → On, set a threshold), or loop manually until the next-link is empty.
Do Until: empty(variables('nextLink'))
HTTP GET variables('nextLink')
Parse JSON the response
Append body('Parse_JSON')?['value'] to your results array
Set variable nextLink = body('Parse_JSON')?['@odata.nextLink']File handling: uploads, downloads & base64
Binary download
A GET that returns a file gives you a binary body. Pass body('HTTP') straight into a Create file action (SharePoint / OneDrive / Blob). To embed it elsewhere, convert with base64(body('HTTP')).
multipart/form-data upload
Headers:
Content-Type: multipart/form-data; boundary=----flowlibs
Body:
------flowlibs
Content-Disposition: form-data; name="metadata"
Content-Type: application/json
{ "title": "Q3 report" }
------flowlibs
Content-Disposition: form-data; name="file"; filename="q3.pdf"
Content-Type: application/pdf
@{base64ToBinary(outputs('Get_file_content')?['body'])}
------flowlibs--Single-file APIs are simpler
Hand-building multipart in the designer is fiddly. Where an API accepts a raw body (e.g. Graph drive upload), just set Content-Type to the file’s type and put the bytes in the body — skip multipart entirely.
base64 in / out
| Expression | Does |
|---|---|
| base64(<binary>) | Binary → base64 string |
| base64ToBinary(<string>) | base64 string → binary (for a request body) |
| base64ToString(<string>) | base64 string → UTF-8 text |
| dataUriToBinary(<dataUri>) | data: URI → binary |
| decodeDataUri(<dataUri>) | data: URI → bytes (keeps content type) |
Large files — chunked upload
- POST to createUploadSession to get a pre-authenticated
uploadUrl. - PUT each byte range to that URL with a
Content-Range: bytes {start}-{end}/{total}header. Every range except the last must be a multiple of 320 KiB (327,680 bytes). - The service returns
202 AcceptedwithnextExpectedRangesbetween chunks, and the final200/201with the created item.
Mind Power Automate’s message limits
A single action’s content is capped (roughly 100 MB, lower in practice). For genuinely large files, stage through Azure Blob or use the chunked upload-session pattern above rather than holding the whole file in a variable.
Inbound webhooks (receiving requests)
The When a HTTP request is received trigger (the *Request* built-in connector — no premium) turns a flow into an endpoint. Save the flow once to generate the POST URL; it includes a SAS signature in the sig query parameter, so treat the whole URL as a secret.
Generate the schema from a sample
Click Use sample payload to generate schema, paste a representative JSON body, and the designer builds the JSON Schema — giving you typed trigger outputs to use downstream without a separate Parse JSON.
The Response action
Add a Response action to reply synchronously with a chosen status code, headers, and body. The caller waits for it (up to the request timeout, ~120s). Without a Response action the trigger returns 202 Accepted immediately and the run continues in the background.
| Code | Meaning | Return when |
|---|---|---|
| 200 | OK | Handled, returning a body |
| 201 | Created | You created a resource |
| 202 | Accepted | Work queued, finishing async |
| 400 | Bad Request | Payload failed validation |
| 401 | Unauthorized | Missing/wrong shared secret |
| 403 | Forbidden | Authenticated but not allowed |
| 429 | Too Many Requests | You are rate-limiting the caller |
| 500 | Server Error | Unhandled failure in the flow |
Securing the URL
- The URL’s
sigtoken is a credential — store it in an environment variable, never log it, and rotate by regenerating the trigger. - Require a shared-secret header (e.g.
x-webhook-secret) and compare it to an env var in a first Condition; Response 401 and Terminate if it does not match. - Validate the payload against the generated schema and reject malformed bodies with
400. - For stronger control, front the endpoint with Azure API Management to add IP allow-listing, rate limits, and signature verification.
Anyone with the URL can trigger the flow
The SAS signature authenticates the request — there is no user identity behind it. Always add your own secret/signature check inside the flow; never paste the trigger URL into tickets, logs, or chats.
Long-running & async patterns
Some APIs accept the work and finish later. They reply 202 Accepted with a Location header that points at a status resource to poll, and often a Retry-After telling you how long to wait.
Poll a 202 + Location
HTTP POST /jobs → 202 Accepted
Set variable statusUrl = outputs('HTTP')?['headers']?['Location']
Do Until: variables('done')
Delay <Retry-After seconds, default e.g. 15s>
HTTP GET variables('statusUrl')
Set done = equals(body('HTTP_2')?['status'], 'succeeded')
(cap the loop count so it cannot run forever)- Read the redirect target from
outputs('HTTP')?['headers']?['Location']. - Honour
Retry-After(seconds, or an HTTP date) for the Delay instead of a fixed sleep. - Always cap iterations and check for a terminal *failed* status, not just *succeeded*.
Webhook callback pattern
Better than polling: hand the API a callback URL (your *When a HTTP request is received* trigger) and let it call you when the work is done. The built-in HTTP Webhook action formalises this — it subscribes on start, parks the run with no polling cost, and unsubscribes on completion or cancellation.
Prefer push over poll
Polling burns action runs and adds latency. If the API offers a subscription/callback, the HTTP Webhook action is cleaner and cheaper than a Do Until.
Throttling: 429, concurrency & batching
APIs push back with 429 Too Many Requests and a Retry-After header. Respect it — retrying immediately just extends the throttle.
- The action’s Default retry policy already honours
Retry-Afteron 408/429/5xx — leave it on for most calls. - Cap Apply to each concurrency (Settings → Concurrency control) when the loop calls a rate-limited API; high parallelism multiplies your 429s.
- Spread load and cache lookups (a Compose/variable) instead of re-calling per row.
$batch (Microsoft Graph)
Combine up to 20 requests into a single POST to /$batch — fewer round-trips and fewer throttles. Each sub-request returns its own status, so a 429 on one does not fail the batch (the batch itself returns 200). Use dependsOn to sequence; a failed dependency returns 424.
{
"requests": [
{ "id": "1", "method": "GET", "url": "/me" },
{ "id": "2", "method": "GET", "url": "/me/drive/root/children" },
{ "id": "3", "method": "GET", "url": "/me/messages?$top=5",
"dependsOn": ["1"] }
]
}Retry only the failed sub-requests
Each sub-request is evaluated against throttling limits individually. Loop the batch responses, and retry just the ones that came back 429 using their own Retry-After — don’t resend the whole batch. See Graph JSON batching.
Retry, timeout & errors
Network calls fail transiently. By default the HTTP action retries on 408 / 429 / 5xx using the Default policy — an exponential interval, up to 4 retries. Tune it under the action’s Settings.
| Policy | Behaviour |
|---|---|
| Default | Exponential, up to 4 retries (scales ~7.5s, capped 5–45s) |
| None | Do not retry — fail fast |
| Fixed Interval | Wait a fixed interval, set count + interval |
| Exponential Interval | Growing random interval, set count + min/max |
"retryPolicy": {
"type": "fixed",
"interval": "PT30S",
"count": 2
}- Set a realistic Timeout (Settings → General → Action Timeout) in ISO 8601, e.g.
PT2M. Default single-request timeout is ~100s. - Wrap the call in a Scope and add a Catch scope configured to run after *has failed / has timed out* (see Best Practices).
- Check the status code explicitly:
outputs('HTTP')?['statusCode']— a 2xx is success, but the action only throws on 4xx/5xx after retries. - Honour
Retry-Afterheaders from APIs that return 429 rather than hammering with your own retries.
Error handling for HTTP
After retries, the action throws on 4xx/5xx. Decide per call: let it fail and catch upstream, or handle inline by reading the status code and branching.
Branch on the status code
Condition: outputs('HTTP')?['statusCode'] is greater than or equal to 400
If yes → read the error body, log, branch or Terminate
If no → continue the happy path
// the status code survives even a "failed" action when you
// set Configure run after → has failed on the next action.Parse the error body
{
"error": {
"code": "Request_ResourceNotFound",
"message": "Resource '…' does not exist or one of its queried references…"
}
}
// body('HTTP')?['error']?['message']| Code | Usual cause | First check |
|---|---|---|
| 400 | Bad request body / query | Schema, $filter syntax, encoding |
| 401 | Token missing or expired | Auth config, audience, secret |
| 403 | Authenticated but no rights | App permission + admin consent |
| 404 | Wrong URL or id | Endpoint path, resource id |
| 429 | Throttled | Honour Retry-After, batch, slow down |
| 5xx | Upstream fault | Retry with backoff, then alert |
Reuse the Try/Catch pattern
For the full Scope-based Try/Catch, result() inspection, and Terminate convention, see the Best Practices guide. Wrap the HTTP call in the Try scope and surface code + message from the Catch.
Common-API cookbook
Copy-paste starting points for the APIs you reach for most. All assume Authentication = Active Directory OAuth (or Managed Identity) with the right audience; add Content-Type: application/json on writes.
Microsoft Graph
POST https://graph.microsoft.com/v1.0/users/{id}/sendMail
{
"message": {
"subject": "Hello",
"body": { "contentType": "HTML", "content": "<p>Hi</p>" },
"toRecipients": [{ "emailAddress": { "address": "ada@contoso.com" } }]
},
"saveToSentItems": true
}
GET https://graph.microsoft.com/v1.0/users?$select=displayName,mail&$top=50
GET https://graph.microsoft.com/v1.0/users/{id}/drive/root/childrenSharePoint REST
GET https://contoso.sharepoint.com/sites/Ops/_api/web/lists/getbytitle('Orders')/items?$select=Title,Status&$top=100
Accept: application/json;odata=nometadataDataverse Web API
GET https://org.crm.dynamics.com/api/data/v9.2/accounts?$select=name,telephone1&$top=50
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json
POST https://org.crm.dynamics.com/api/data/v9.2/accounts
{ "name": "Contoso Ltd", "telephone1": "555-0100" }Azure DevOps
GET https://dev.azure.com/{org}/{project}/_apis/wit/workitems/{id}?api-version=7.1
Accept: application/jsonGeneric REST CRUD
| Verb | URL | Body | Returns |
|---|---|---|---|
| GET | /v1/orders?$top=50 | — | 200 + list |
| POST | /v1/orders | { … new order … } | 201 + created |
| PATCH | /v1/orders/{id} | { … changed fields … } | 200/204 |
| DELETE | /v1/orders/{id} | — | 204 |