Almost every piece of software that talks to Dynamics 365 CE today does it through the Dataverse Web API — Dataverse being the database and platform underneath Dynamics 365. Whether it's a script pulling data into a spreadsheet, a website's background code fetching related records, or an automated flow's underlying calls, it's all going through the same doorway. That doorway is a standard REST API (a common, well-understood way for one piece of software to ask another for data over the web) built on OData v4, a standard set of conventions for describing those requests. That means a lot of what you may already know about calling APIs applies directly here, but there are a handful of Dataverse-specific habits worth learning before you start. This tutorial covers the practical mechanics — from basic reading and writing through bundling many changes together and connecting real external systems.

What the Web API is, and what it replaced

Before the Web API existed, the main way to talk to Dynamics 365 from outside a plugin was the Organization Service — an older, more rigid style of connection (SOAP-based, for those who know the term) that mostly required a heavy .NET toolkit (an SDK, or software development kit) just to get started. The Web API is the modern replacement: a web address like https://yourorg.crm.dynamics.com/api/data/v9.2/ that any programming language capable of making a basic web request can talk to — not just .NET.

That matters in practice because it opened Dynamics 365 up to a much wider range of tools — scripts written in Python, small services in Node.js, automated cloud workflows, mobile apps — without anyone needing to install a specialized toolkit first. The older Organization Service connection still exists and still gets used, especially inside plugins where the platform already hands your code an authenticated connection for free, but for anything outside the platform, the Web API is the standard route today.

Core actions, mapped to a restaurant order

The Web API uses four familiar types of web requests, and they map neatly onto ordering food at a restaurant:

Request typeWhat it doesRestaurant equivalent
GETRead a record, or run a searchLooking at the menu, or asking what's in the kitchen
POSTCreate a new recordPlacing a brand-new order
PATCHUpdate part of an existing recordAsking the waiter to change one item on an order you already placed
DELETERemove a recordCancelling the order

A couple of Dataverse-specific habits are worth knowing early. To connect one record to another — say, setting which Account a Contact belongs to — you don't just write a plain ID into a field; you write it using a special "@odata.bind" syntax that points at the other record's own web address, something like "customerid_account@odata.bind": "/accounts(00000000-0000-0000-0000-000000000000)". And the web addresses for record types are usually just the plural of their name (accounts, contacts, opportunities), which occasionally surprises people the first time they hit a record type with an unusual plural.

Asking for exactly what you need: query options

When you're reading data with a GET request, a handful of extra instructions — tacked onto the web address itself — control exactly what comes back. Using them well is the difference between an API call that returns exactly what a screen needs, and one that drags back far more than necessary, like ordering the entire menu when you only wanted a sandwich:

GET [Organization URI]/api/data/v9.2/accounts
    ?$select=name,revenue,telephone1
    &$filter=statecode eq 0 and revenue gt 500000
    &$expand=primarycontactid($select=fullname,emailaddress1)
    &$orderby=revenue desc
    &$top=25

One behavior worth knowing up front: if a search would return a lot of records, the Web API automatically splits the results into pages and hands back a link (@odata.nextLink) to fetch the next page, rather than returning everything at once. Code that assumes one response contains the full answer will quietly under-report results once the data grows — always check for that link and follow it if the complete set matters.

Three languages, one destination: FetchXML, QueryExpression, and Web API queries

Dataverse lets you ask the same question three different ways — a bit like asking for directions in three different languages that all get you to the same place. Which one you reach for depends on where your code is running:

FetchXMLQueryExpressionWeb API (OData)
Looks likeA block of XMLObjects built up in .NET codeA web address with query text tacked on
Typically used inSaved views, reports, or embedded inside other calls.NET code such as pluginsExternal apps, websites, anything outside .NET
Can it total or group things up?Yes — sums, counts, averages, group-byYes, with its own aggregate settingsLimited — possible but less commonly used
Works outside its home tool?Yes — the same XML works in several placesNo — tied to .NETYes — standard OData, works from any web request tool

In practice: if you're already writing .NET code inside a plugin, QueryExpression tends to read more naturally and catches typos before you even run it. If you need a query that has to work as a saved view, a report, and possibly get reused elsewhere, FetchXML is the common language everything understands — and handily, the Web API will accept a FetchXML query directly as a parameter on a GET request, which is a useful escape hatch when a plain web-address query would otherwise get unreasonably complicated (deep nested filters, certain totals). For anything living outside Dynamics 365 entirely, plain Web API query options are usually the simplest choice and need no XML at all.

Batch requests: bundling many actions into one trip

The Web API lets you bundle several actions into a single request instead of sending them one at a time — a bit like handing the waiter a full list of changes at once instead of calling them over individually, using the $batch endpoint. This helps for two reasons: it cuts down on back-and-forth trips when you have many small operations to do, and, when bundled a certain way (as a "changeset"), all of the actions inside succeed or fail together as one unit — if one part fails, none of it is kept.

This is the right tool when related records need to go in together and stay consistent — an order and its line items, for example — without writing custom server-side code just to enforce that they rise or fall as one. It's overkill for a single one-off update, and most toolkits used to call the Web API already come with a built-in helper for building these bundles, so you rarely have to construct one by hand.

Calling the Web API from an outside application

A common setup is a standalone program or scheduled job that signs in (through Azure AD, also known as Microsoft Entra ID — Microsoft's identity service) and then talks to the Web API directly over the web to read or write Dynamics 365 records — no plugin, no special toolkit, just a basic web request with a security token attached. Here's a minimal, illustrative example that updates a contact's phone number:

using var client = new HttpClient { BaseAddress = new Uri(orgUrl) };
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
client.DefaultRequestHeaders.Add("OData-Version", "4.0");
client.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("application/json"));

var body = new StringContent(
    "{ \"telephone1\": \"555-0148\" }",
    Encoding.UTF8, "application/json");

var request = new HttpRequestMessage(
    HttpMethod.Patch,
    $"api/data/v9.2/contacts({contactId})")
{
    Content = body
};

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();

Strip away the specifics and the pattern is simple: build a request with the right action type and web address, attach the standard headers, include a small chunk of JSON (a common, simple text format for structured data) describing what's changing, and send it. Most real integrations wrap this in a small reusable helper rather than writing raw requests everywhere, but underneath, the mechanics are exactly this straightforward.

Authentication: proving who you are without a human logging in

Server-to-server connections — one system talking to another with nobody sitting at a keyboard — authenticate using an Azure AD app registration: essentially, an identity created for the piece of software itself rather than for a person, paired with a matching "application user" inside Dynamics 365. The software proves who it is with a client ID and a secret (or a certificate), not a username and password, following what's called the OAuth 2.0 client credentials flow. That app registration gets a security role in Dynamics 365 just like a real employee's keycard would, controlling exactly what the integration is and isn't allowed to touch — following the same access rules covered in the fundamentals tutorial.

This is a conceptual overview only — actual client secrets, certificates, and tenant details should always be stored in a secrets manager (such as Azure Key Vault) and never hard-coded or checked into source control, regardless of environment.

The practical benefit over signing in as a real person is stability: a connection tied to a human's own login breaks the moment that person changes their password or leaves the company, while an app registration's credentials are managed completely independently of any one person — a detail that matters more than it sounds, the first time an integration mysteriously stops working after an unrelated HR change.

Common integration patterns

A handful of shapes come up again and again across Dynamics 365 CE integration projects, regardless of industry:

Choosing between "check for changes on a schedule" and "push updates the moment something happens" is one of the more consequential integration decisions on a project. Checking on a schedule is simpler to build and reason about, but adds delay and does unnecessary work when nothing has actually changed. Pushing updates the instant something happens is more responsive, but adds moving parts — queues, retry logic, and making sure the same update never gets applied twice — that need to be planned for deliberately rather than patched in after something breaks in production.