MSAZ-300 · Lesson 11 of 12 · 21 min
Calling REST endpoints from the database
▶ Video coming soon A video version of this lesson is on the way.
When the nightly tier recalculation from lesson 4 moves a member up to Kestrel, marketing wants the member to get a congratulations message in the app the next morning. The message service is an Azure Function, fn-finch-notify, that the app team already runs. Hana's first idea was a scheduled job in the API that polls the database for tier changes. Elena showed her two other ways: the database can call the function's REST endpoint directly, or the database's changes can be streamed out to other services as events. In the eleventh session they tried both, and added the third piece DP-800 covers in this area, Data API builder, which turns database objects into REST and GraphQL endpoints without hand-written API code.
sp_invoke_external_rest_endpoint
The system stored procedure sp_invoke_external_rest_endpoint sends an HTTPS request from the database and returns the response. It is enabled by default in Azure SQL Database and SQL database in Fabric. In SQL Server 2025 and in Managed Instance with the SQL Server 2025 or Always-up-to-date update policy it is disabled by default, and an administrator turns it on with sp_configure 'external rest endpoint enabled'. The caller needs the EXECUTE ANY EXTERNAL ENDPOINT database permission, and REFERENCES on any database scoped credential the call uses.
DECLARE @ret int, @response nvarchar(max);
DECLARE @payload nvarchar(max) =
JSON_OBJECT('memberId': CAST(@PublicId AS nvarchar(36)),
'newTier': 'K',
'effective': CONVERT(char(10), SYSUTCDATETIME(), 23));
EXECUTE @ret = sp_invoke_external_rest_endpoint
@url = N'https://fn-finch-notify-cus.azurewebsites.net/api/tier-changed',
@method = 'POST',
@payload = @payload,
@credential = [https://fn-finch-notify-cus.azurewebsites.net/api/tier-changed],
@timeout = 10,
@retry_count = 2,
@response = @response OUTPUT;
SELECT @ret AS ReturnCode,
JSON_VALUE(@response, '$.response.status.http.code') AS HttpStatus,
JSON_VALUE(@response, '$.result.messageId') AS MessageId;The parameters follow the shape of an HTTP request. @url must be HTTPS; plain HTTP is refused and redirects are not followed. @method defaults to POST and accepts GET, POST, PUT, PATCH, DELETE and HEAD. @headers takes a flat JSON object, @payload the request body, and @timeout a number of seconds from 1 to 230, 30 by default. @retry_count, from 0 to 10, retries on status codes such as 429 and 503, honoring a Retry-After header when the service sends one. The return value is 0 for a 2xx status, otherwise the status code received, and an error is raised if the call cannot be made at all.
The @response output is a JSON document with two parts. The response part holds the HTTP status and the response headers, and the result part holds the body the service returned, omitted for a 204 No Content. Because it is JSON, the functions from lesson 2 read it: JSON_VALUE for single values and OPENJSON for arrays. Payloads can be up to 100 MB in each direction, URLs up to 8 KB and headers up to 8 KB in total.
| Parameter | Default | Notes |
|---|---|---|
| @url | Required | HTTPS only; no redirects; up to 8 KB |
| @method | POST | GET, POST, PUT, PATCH, DELETE or HEAD |
| @headers | None | A flat JSON object; some headers are set by the system |
| @payload | None | JSON, XML or text; must be well formed |
| @credential | None | A database scoped credential whose name is a URL prefix of @url |
| @timeout | 30 seconds | 1 to 230 seconds, cumulative across retries |
| @retry_count | 0 | 0 to 10 retries on throttling and server errors |
| @response | Output | JSON with response (status, headers) and result (the body) |
Credentials and the allowlist
Authentication goes in a database scoped credential, not in the T-SQL that makes the call. The credential's name must be a URL, with no query string, that matches the protocol and host of the call and is the same as, or a prefix of, its path. Its IDENTITY says how the secret is sent. HTTPEndpointHeaders sends key-value pairs as request headers, which suits an Azure Functions key. HTTPEndpointQueryString sends them in the query string. Managed Identity sends a token for the database's managed identity, which is the best choice whenever the target service accepts Entra tokens, because there is no secret to store or rotate. Shared Access Signature suits storage.
-- Preferred: the logical server's managed identity, no secret to store
CREATE DATABASE SCOPED CREDENTIAL [https://fn-finch-notify-cus.azurewebsites.net/api/tier-changed]
WITH IDENTITY = 'Managed Identity',
SECRET = '{"resourceid": "api://fn-finch-notify"}';
GRANT EXECUTE ANY EXTERNAL ENDPOINT TO [app-finchrewards-jobs];
GRANT REFERENCES ON DATABASE SCOPED CREDENTIAL::[https://fn-finch-notify-cus.azurewebsites.net/api/tier-changed]
TO [app-finchrewards-jobs];Database scoped credentials need a database master key to protect their secrets, created once with CREATE MASTER KEY. In Azure SQL Database and Managed Instance, calls are limited to an allowlist of domains: Azure Functions and App Service, Logic Apps, Event Hubs and Service Bus, storage, Azure OpenAI, Key Vault, API Management, Microsoft Graph, Azure AI Search, Dynamics and several more. A service outside the list, such as a shipping company's address API, can be reached by putting Azure API Management in front of it. Outbound firewall rules on the logical server can narrow it further to exactly the hosts a database may call, which Sunil required for production.
When not to call out
Calling a web service from T-SQL is convenient and easy to misuse. The call waits, and while it waits the session holds whatever locks it has. A procedure that updates a member and then calls a slow service before committing keeps that member row locked for the duration, up to the timeout. Elena's rule is that a REST call never happens inside a transaction that holds locks other work needs. The tier procedure commits the tier changes first, writes each change to an outbox table in the same transaction, and a separate step reads the outbox and calls the function, marking each row as sent. If the function is down, the outbox keeps the messages and the next run sends them.
Concurrency is limited too. External calls can use at most 15 in 100 of the database's worker threads, and never more than 175, and beyond that the procedure fails with error 10928, or 10936 for an elastic pool. A per-row loop that calls a service for each of 412,000 members would be slow and would hit that limit. The outbox step sends changes in batches, about 500 per call, because fn-finch-notify accepts a JSON array.
-- Send unsent tier changes in batches of 500 from the outbox
DECLARE @batch nvarchar(max), @ret int, @response nvarchar(max);
SELECT @batch = (SELECT TOP (500) OutboxId, MemberPublicId, NewTier
FROM rewards.TierOutbox
WHERE SentAtUtc IS NULL
ORDER BY OutboxId
FOR JSON PATH);
IF @batch IS NOT NULL
BEGIN
EXECUTE @ret = sp_invoke_external_rest_endpoint
@url = N'https://fn-finch-notify-cus.azurewebsites.net/api/tier-changed-batch',
@payload = @batch,
@credential = [https://fn-finch-notify-cus.azurewebsites.net/api/tier-changed],
@timeout = 30, @retry_count = 3, @response = @response OUTPUT;
IF @ret = 0
UPDATE o SET SentAtUtc = SYSUTCDATETIME()
FROM rewards.TierOutbox AS o
JOIN OPENJSON(@batch) WITH (OutboxId bigint) AS b ON b.OutboxId = o.OutboxId;
END;Streaming changes instead
The outbox works, but it is a pattern the team must build and maintain. Change event streaming, CES, does the same job inside the engine: it publishes inserts, updates and deletes on chosen tables as CloudEvents, in JSON or Avro, to Azure Event Hubs or Fabric Eventstream, close to real time, without triggers or polling. Other services subscribe to the events. At the time of writing CES is a preview feature in SQL Server 2025, Azure SQL Database and Managed Instance, so Elena kept the outbox for production and set up a CES test against the development database to compare.
The DP-800 outline lists the other ways to react to changes, and each fits a different need. Change data capture, CDC, reads the transaction log and writes every change to change tables that consumers query; it keeps the old and new values and suits data integration. Change tracking records only which rows changed and a version number, not the values; it suits synchronizing a cache or a mobile app that will fetch the current row anyway. The Azure Functions SQL trigger binding runs a function when rows in a table change, built on change tracking. Logic Apps can poll for changes and run a workflow with little code.
| Method | What it captures | How consumers get changes | Good for |
|---|---|---|---|
| Change event streaming (CES) | Row changes with values, as CloudEvents | Pushed to Event Hubs or Fabric Eventstream | Near-real-time integration; preview at the time of writing |
| Change data capture (CDC) | Old and new values from the log | Query change tables | Data integration and auditing of values |
| Change tracking | Which rows changed, with versions | Query changes since a version | Cache and app synchronization |
| Azure Functions SQL trigger | Row changes, via change tracking | The function runs automatically | Small event handlers without polling code |
| Outbox table and REST call | What the procedure writes | A job calls the endpoint | Full control, any target on the allowlist |
Data API builder
Data API builder, DAB, is an open-source tool from Microsoft that serves REST and GraphQL endpoints for database objects described in a JSON configuration file. Each entity in the file maps a name to a table, view or stored procedure, and lists which roles may read, create, update, delete or execute it. DAB handles pagination, filtering, sorting and selecting fields from query string parameters for REST and from arguments for GraphQL, can cache responses, and supports relationships between entities so a GraphQL query can fetch a member with their recent transactions. It runs as a container, in Azure Container Apps or App Service, or locally for development, and authenticates callers with Entra ID or another provider. Recent versions can also host an MCP server, which is how lesson 10's Copilot tools reach the development database.
{
"data-source": { "database-type": "mssql",
"connection-string": "@env('FINCHREWARDS_SQL')" },
"runtime": { "rest": { "path": "/api" }, "graphql": { "path": "/graphql" } },
"entities": {
"Offer": {
"source": { "object": "rewards.vActiveOffers", "type": "view", "key-fields": ["OfferId"] },
"permissions": [ { "role": "authenticated", "actions": ["read"] } ],
"cache": { "enabled": true, "ttl-seconds": 60 }
},
"PostPurchase": {
"source": { "object": "rewards.uspPostPurchase", "type": "stored-procedure" },
"permissions": [ { "role": "store-system", "actions": ["execute"] } ]
}
}
}For Finch Rewards, DAB replaced about a third of the hand-written API: the read-only endpoints for offers, stores and the member card come straight from views, and the write endpoints call the procedures from lessons 4 and 5, so the rules stay in the database. The connection string is read from an environment variable that holds no password, because DAB connects with its own managed identity. The remaining API code handles sign-in, the app's business flows and anything that combines several services.
Worked problem
How many calls does the outbox make, and how long can it take?
On a typical night the tier recalculation moves about 2,300 members between tiers. The outbox step sends batches of 500. Each call takes about 0.8 seconds when the function is healthy. On a bad night every call is throttled once and retried after the 2-second Retry-After the function sends. How many calls are made, and how long does the step take on a typical and on a bad night?
Show solution
- Batches: 2,300 ÷ 500 = 4.6, so 5 calls (four full batches and one of 300).
- Typical night: 5 × 0.8 s = 4.0 s.
- Bad night: each batch makes 2 attempts with a 2-second wait between them: 5 × (0.8 + 2 + 0.8) = 5 × 3.6 = 18 s.
- Both are well inside the 30-second timeout per call, and the job runs at 03:00 when nothing else is waiting on it.
Answer: Five calls; about 4 seconds on a typical night and about 18 seconds when every call is retried once.
Common mistakes
MistakeCall the web service inside the transaction that updates the member, so they stay in step.
InsteadCommit first and use an outbox; a slow call inside a transaction holds locks.
MistakePut the function key in the T-SQL that calls the endpoint.
InsteadUse a database scoped credential, ideally with a managed identity.
Mistakesp_invoke_external_rest_endpoint can call any HTTPS address from Azure SQL Database.
InsteadOnly allowlisted domains; use API Management for others and narrow further with outbound firewall rules.
MistakeChange tracking records old and new values.
InsteadChange tracking records which rows changed; CDC and CES carry values.
MistakeData API builder means the rules must move into the app.
InsteadDAB can expose views and procedures, so the rules stay in the database.
Summary and key terms
- sp_invoke_external_rest_endpoint makes HTTPS calls from T-SQL and returns a JSON response.
- It is on by default in Azure SQL Database and Fabric, off by default in SQL Server 2025 and Managed Instance.
- Callers need EXECUTE ANY EXTERNAL ENDPOINT and REFERENCES on the credential.
- Credentials are database scoped, named by URL, and best use a managed identity.
- Azure SQL limits calls to an allowlist; outbound firewall rules narrow it further.
- Avoid calls inside lock-holding transactions; use an outbox and batches.
- CES, CDC, change tracking, Functions SQL triggers and Logic Apps react to changes in different ways.
- Data API builder serves REST and GraphQL from a JSON configuration with per-role permissions.
- sp_invoke_external_rest_endpoint
- A system stored procedure that calls an HTTPS REST or GraphQL endpoint from the database.
- Database scoped credential
- A credential stored in a database and used to authenticate outbound calls.
- Outbox pattern
- Writing outgoing messages to a table in the same transaction as the change, and sending them separately.
- Change event streaming
- An engine feature that publishes row changes as events to Event Hubs or Fabric Eventstream.
- Change data capture
- A feature that records old and new values of changed rows from the transaction log.
- Change tracking
- A feature that records which rows changed, without their values.
- Data API builder
- An open-source tool that serves REST and GraphQL endpoints for database objects from a configuration file.
Practice problems
Work each problem on paper first, then reveal the answer to check.
What does a return value of 0 from sp_invoke_external_rest_endpoint mean?
Show answer
The call completed with a 2xx HTTP status.
Which credential identity avoids storing a secret?
Show answer
Managed Identity.
How can a database in Azure SQL reach a service whose domain is not allowlisted?
Show answer
Through Azure API Management in front of the service.
Why does the tier procedure write to an outbox instead of calling the function directly?
Show answer
So the call happens after commit, without holding locks, and failed sends are retried.
Which change method suits a mobile app that only needs to know which rows to refetch?
Show answer
Change tracking.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.