A reference guide for building AI agents: every method, how to authenticate, and the permissions each one needs.
The Microsoft Dynamics 365 API is how an app or AI agent works with the Dataverse data behind the business apps: reading and updating accounts and contacts, qualifying a lead, creating an opportunity, or resolving a customer case. Access is granted through a Microsoft Entra ID sign-in, and what the agent can reach is governed by Dataverse security roles and table and column privileges rather than per-endpoint scopes. A record change can be detected through change tracking or pushed to an external system by a registered plug-in or flow.
How an app or AI agent connects to Dynamics 365 determines what it can reach. There is the Dataverse Web API for making calls, a hosted server that exposes Dynamics 365 tools to agents, and change notifications for learning about activity, and each route runs as a Dataverse user whose security roles set what data the agent can touch.
The Dataverse Web API is an OData version 4.0 REST service at [Organization URI]/api/data/v9.2/. It accepts JSON, returns JSON, and uses GET to retrieve and call functions, POST to create and call actions, PATCH to update and upsert, and DELETE to remove. Records live in entity sets named by EntitySetName (accounts, contacts, leads, opportunities, incidents), and queries use OData options such as $select, $filter, $orderby, $top, and $expand.
Dataverse provides a first-party Model Context Protocol server that exposes its tables and records to AI agents, generally available at [Organization URI]/api/mcp (preview at /api/mcp_preview). A non-Microsoft client such as Claude connects through the @microsoft/dataverse npm local proxy or directly to the remote endpoint with a registered Microsoft Entra app granted the Dynamics CRM mcp.tools permission. It exposes tools for listing tables, describing a table's schema, querying records, and creating or updating rows, and must be enabled per environment with its allowed clients.
Dataverse does not offer a generic outbound webhook on every table. To learn about changes, an agent adds the Prefer: odata.track-changes header to a query and receives a delta token to fetch only what changed since last time. To push a change to an external endpoint as it happens, register a plug-in or webhook step on a table's create, update, or delete message, or build a Power Automate flow on that event.
An interactive app signs a person in with OAuth 2.0 through Microsoft Entra ID and acts on their behalf, requesting the environment-URL plus /user_impersonation scope. The app needs the Access Dynamics 365 as organization users delegated permission. Every call runs with that person's Dataverse security roles, so the agent can reach only what they can.
A background or scheduled app authenticates with no interactive user, requesting the environment-URL plus /.default scope and proving itself with a client secret or X.509 certificate. It is bound to a dedicated Dataverse application user that is assigned a custom security role, which defines exactly what the app can do. This route does not consume a paid user license.
The Dynamics 365 API works over Dataverse tables, the records behind the business apps, like accounts, contacts, leads, opportunities, and cases. Each table is a collection an agent can read, create, update, or delete, and named actions and functions trigger built-in business logic such as winning an opportunity or resolving a case.
Create, retrieve, update, upsert, and delete records in any Dataverse table.
Read collections with OData query options or with FetchXML.
Relate and unrelate records across table relationships.
Call built-in messages and group requests into a single batch.
Read the definitions of tables, columns, and relationships at run time.
Filter by method, access, or permission, or search any path. Select a row for version detail, rate limits, the related webhook event, and the source.
| Method | Endpoint | What it does | Access | Permission | Version | |
|---|---|---|---|---|---|---|
Records (CRUD)Create, retrieve, update, upsert, and delete records in any Dataverse table.7 | ||||||
| POST | /api/data/v9.2/accounts | Create a record in a table by posting JSON to its entity set, for example accounts. | write | Create privilege | Current | |
Needs the Create privilege on the target table. Add Prefer: return=representation to get the created record back as 201. Acts ontable record Permission (capability) Create privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| POST | /api/data/v9.2/accounts | Create a record together with related records in one atomic operation (deep insert), or bind to existing records with @odata.bind. | write | Create privilege | Current | |
Needs the Create privilege on each table involved. The whole insert succeeds or fails together. Acts ontable record Permission (capability) Create privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| GET | /api/data/v9.2/accounts(id) | Retrieve a single record by its primary key, using $select to limit columns. | read | Read privilege | Current | |
Read-only. Can also be retrieved by an alternate key. Always use $select for performance. Acts ontable record Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| PATCH | /api/data/v9.2/accounts(id) | Update a record by sending only the columns being changed. Add If-Match: * to avoid an accidental create. | write | Write privilege | Current | |
Needs the Write privilege on the table. Include only changed columns to avoid triggering unintended business logic. Acts ontable record Permission (capability) Write privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| PATCH | /api/data/v9.2/accounts(id) | Upsert a record: create it if it does not exist, update it if it does. Often keyed by an alternate key from an external system. | write | Write privilege | Current | |
Needs Create and Write privileges, since either may occur. Use If-Match or If-None-Match to prevent one of the two outcomes. Acts ontable record Permission (capability) Write privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| DELETE | /api/data/v9.2/accounts(id) | Permanently delete a record by its primary key. | write | Delete privilege | Current | |
Needs the Delete privilege. Returns 204 if deleted, 404 if the record was not found. Acts ontable record Permission (capability) Delete privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| PUT | /api/data/v9.2/accounts(id)/name | Update a single column value by appending the column name to the record URI. | write | Write privilege | Current | |
Needs the Write privilege. PUT is for individual properties, not whole records. Acts ontable column value Permission (capability) Write privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
QueryRead collections with OData query options or with FetchXML.3 | ||||||
| GET | /api/data/v9.2/accounts?$select=&$filter= | Retrieve multiple records from a table, refined with OData options like $select, $filter, $orderby, $top, and $expand. | read | Read privilege | Current | |
Read-only. Returns up to 5,000 rows per page; the $skip and $search options are not supported. Acts ontable collection Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| GET | /api/data/v9.2/accounts?fetchXml= | Run a FetchXML query by passing a URL-encoded FetchXML string in the fetchXml parameter, an alternative to OData for complex queries. | read | Read privilege | Current | |
Read-only. FetchXML supports joins and aggregations OData cannot; send long queries inside a $batch to beat the URL length limit. Acts ontable collection Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| GET | /api/data/v9.2/accounts/$count | Get a count of records related through a collection-valued navigation property, or use $count with a query. | read | Read privilege | Current | |
Read-only. The standard count is capped at 5,000; request the totalrecordcountlimitexceeded annotation to know if more match. Acts ontable collection Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
Associate & disassociateRelate and unrelate records across table relationships.3 | ||||||
| PATCH | /api/data/v9.2/contacts(id) | Associate a record to a related record across a many-to-one relationship by setting a single-valued navigation property with @odata.bind. | write | Append privilege | Current | |
Needs Append on the referencing table and AppendTo on the referenced table. Set the property to null to disassociate. Acts onrelationship Permission (capability) Append privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| POST | /api/data/v9.2/accounts(id)/contact_customer_accounts/$ref | Add a record to a collection-valued navigation property to relate it, used for one-to-many and many-to-many relationships. | write | Append privilege | Current | |
Needs Append and AppendTo privileges. The related record is referenced by an absolute @odata.id URI. Acts onrelationship Permission (capability) Append privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| DELETE | /api/data/v9.2/accounts(id)/contact_customer_accounts(relatedid)/$ref | Remove a record from a collection-valued navigation property to unrelate two records. | write | Append privilege | Current | |
Needs Append and AppendTo privileges. Removes the link only, not the records themselves. Acts onrelationship Permission (capability) Append privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
Functions, actions & batchCall built-in messages and group requests into a single batch.5 | ||||||
| GET | /api/data/v9.2/WhoAmI | Call an unbound function. WhoAmI returns the calling user's UserId, BusinessUnitId, and OrganizationId. | read | — | Current | |
Functions are called with GET and have no side effects. WhoAmI needs no special privilege; other functions read data subject to the user's role. Acts onfunction Permission (capability)None required VersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| GET | /api/data/v9.2/systemusers(id)/Microsoft.Dynamics.CRM.RetrieveUserPrivileges | Call a function bound to a table by appending its full namespaced name to the record URI, such as RetrieveUserPrivileges on a user. | read | Read privilege | Current | |
Bound functions require the full Microsoft.Dynamics.CRM namespace prefix and a record URI for the first parameter. Acts onfunction Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| POST | /api/data/v9.2/Merge | Call an unbound action that changes data, such as Merge to combine a pair of duplicate records. | write | Write privilege | Current | |
Actions are called with POST and can have side effects. Merge needs the privileges its underlying operation requires on the records involved. Acts onaction Permission (capability) Write privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| POST | /api/data/v9.2/queues(id)/Microsoft.Dynamics.CRM.AddToQueue | Call an action bound to a table by appending its full namespaced name to the record URI, such as AddToQueue. | write | Write privilege | Current | |
Bound actions require the full Microsoft.Dynamics.CRM namespace prefix and a record URI for the first parameter. Custom APIs are called the same way. Acts onaction Permission (capability) Write privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| POST | /api/data/v9.2/$batch | Group up to 1,000 requests into one HTTP call, optionally as an all-or-nothing transaction using a change set. | write | — | Current | |
Sent as multipart/mixed. Each request inside the batch is checked against the user's privileges individually; it does not bypass service protection limits. Acts onbatch Permission (capability)None required VersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
MetadataRead the definitions of tables, columns, and relationships at run time.2 | ||||||
| GET | /api/data/v9.2/EntityDefinitions | Read table definitions (metadata): display names, entity set names, and other schema, optionally filtered to one table by LogicalName. | read | Read privilege | Current | |
Read-only. Returns all matches with no paging; use $select because metadata records are large. Expand Attributes for column definitions. Acts ontable definition Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
| GET | /api/data/v9.2/RelationshipDefinitions | Read relationship definitions (metadata) describing how tables relate to each other. | read | Read privilege | Current | |
Read-only. Cast to OneToManyRelationshipMetadata or ManyToManyRelationshipMetadata to read type-specific properties. Acts onrelationship definition Permission (capability) Read privilegeVersionAvailable since the API’s base version Webhook eventNone Rate limitStandard limits apply SourceOfficial documentation ↗ | ||||||
Dynamics 365 can keep an app informed of data that changed without re-reading everything. Change tracking lets an agent ask for only the records added, updated, or deleted since its last sync, and registered plug-ins or Power Automate flows can push a record change out to an external endpoint.
| Event | What it signals | Triggered by |
|---|
Dynamics 365 limits how fast each user can call the API through service protection limits measured over a rolling five-minute window, counting the number of requests, their combined run time, and how many run at once.
Dynamics 365 applies service protection limits per user over a rolling five-minute (300-second) window, evaluated on three facets: the number of requests, their combined execution time, and how many run at once. The defaults per web server are 6,000 requests, 20 minutes (1,200 seconds) of combined execution time, and 52 concurrent requests; most environments have several web servers. Going over returns HTTP 429 Too Many Requests with a Retry-After header giving the seconds to wait. These limits are separate from licensing entitlement limits, and Dataverse search uses a different API with its own one-request-per-second-per-user limit.
An OData query returns up to 5,000 standard-table rows (500 for elastic tables) per page. Set a smaller page size with the Prefer: odata.maxpagesize header, then follow the @odata.nextLink in the response to fetch the next page. Use $top to cap the total rows returned instead of paging. The $skip and $search query options are not supported.
A single request URL is capped at 32 KB (32,768 characters), which a long FetchXML or OData query can hit; sending the request inside a $batch raises the limit to 64 KB. Any individual OData segment is capped at 260 characters. A single $batch request can contain up to 1,000 operations.
The status codes an agent should handle, and what to do about each.
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | BadRequest | The request is invalid, for example a malformed query, a bad argument, or a URL or OData segment that exceeds the length limit. | Read the error message, fix the request, and resend. Use parameter aliases for long or special-character function parameters. |
| 401 | Unauthorized | No valid authentication was provided, or the token is expired or tampered with (for example ExpiredAuthTicket or RequestIsNotAuthenticated). | Acquire a fresh Microsoft Entra ID access token and send it as a Bearer token. Tokens expire after about 60 minutes. |
| 403 | Forbidden (PrivilegeDenied) | The user is authenticated but their security role lacks the privilege the request needs, for example a missing Create, Read, or column-level permission. | Grant the required privilege on the user's or application user's security role, then retry. |
| 404 | Not Found | The requested record or resource does not exist, or is not visible to this user. | Verify the record ID and entity set name, and confirm the record exists in this environment. |
| 412 | Precondition Failed | A concurrency or duplicate conflict, such as ConcurrencyVersionMismatch when an ETag no longer matches, or DuplicateRecord. | Re-retrieve the record to get the current ETag, reconcile, and retry. Resolve duplicates as appropriate. |
| 429 | Too Many Requests | A service protection limit was hit: too many requests, too much combined execution time, or too many concurrent requests in the five-minute window. | Wait the number of seconds in the Retry-After header, then retry. Smooth the request rate and reduce concurrency. |
| 500 | Server Error | An error occurred on the Dataverse side. It can also appear as 501 Not Implemented or 503 Service Unavailable. | Retry with backoff. If it persists, contact Microsoft support. |
Dynamics 365 pins a version number in the path of every Dataverse Web API call, and a version stays stable so code written against it keeps working as new capabilities arrive under newer versions.
v9.2 is the current Web API version, pinned in the path of every call (for example /api/data/v9.2/). The v9.0, v9.1, and v9.2 releases share identical behavior with no breaking changes between them; new capabilities are added without removing existing ones. Microsoft mints a new version number only when it must make a change that is not backward compatible.
Starting with the v9.0 release, the Web API supports version-specific differences in the same environment, so the version in the service URL became meaningful. New access-sharing operations such as GrantAccess, ModifyAccess, and RetrieveSharedPrincipalsAndAccess were added. FetchXML query responses stopped encoding special characters (for example '.' no longer becomes 'x002e'), and a table and a same-named column are no longer disambiguated by appending '1'.
In the v8.x line, every minor version behaved identically because all changes were additive, so the version referenced in the URL did not matter. After an upgrade to v8.2, the v8.0 and v8.1 services were all the same. This line predates the version-specific behavior introduced at v9.0.
Pin v9.2 in the path and move up deliberately after checking documented differences.
Dataverse Web API versions ↗Bollard AI sits between a team's AI agents and Dynamics 365. Grant each agent exactly the access it needs, read or write, table by table, and every call is checked and logged.