Our middleware application authenticates successfully with Azure AD and retrieves an OAuth2 bearer token, but when we attempt to POST journal entries to the GeneralJournalAccountEntry endpoint, we get a 401 Unauthorized response. The same token works fine for GET operations on the same endpoint.
POST /data/GeneralJournalAccountEntry
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJh...
Content-Type: application/json
The token has the required Dynamics.ERP.All scope and our app registration has API permissions for Dynamics 365. Is there an additional permission or configuration needed specifically for journal entry posting? We’re on version 10.0.39.
Exactly right on the flow. The CSRF token lifecycle is tied to your session, not the OAuth token. If you’re making multiple POST requests, you can reuse the CSRF token until you get a 403 response, then fetch a new one. Also verify that your app registration has delegated permissions, not just application permissions - journal posting through OData typically requires delegated access context.
Here’s the complete solution for REST API authentication with journal entry posting:
OAuth2 Token Configuration
Your Azure AD app registration needs:
- API Permissions: Dynamics 365 ERP > Delegated permissions > Dynamics.ERP.All
- Authentication: Enable ‘Access tokens’ and ‘ID tokens’ under Implicit grant
- Redirect URI: Configure appropriate callback for your middleware
Grant admin consent for the permissions in Azure Portal.
D365 Service Principal Setup
In System Administration > Users > Azure Active Directory applications:
- Add your Azure AD app’s Application (client) ID
- Map it to a D365 user account (create dedicated service account)
- Assign security roles: Accountant + System User minimum
- Verify in General Ledger > Setup > Journal Names that the service account has access to target journal names
REST API Request Pattern
Journal entry posting requires a two-step authentication:
Step 1 - Fetch CSRF Token:
GET {baseUrl}/data/GeneralJournalAccountEntry
Authorization: Bearer {oauth_token}
Extract ‘OData-EntityId’ from response headers.
Step 2 - POST with CSRF Token:
POST {baseUrl}/data/GeneralJournalAccountEntry
Authorization: Bearer {oauth_token}
X-CSRF-Token: {csrf_token}
Content-Type: application/json
Journal Entry Payload Requirements
Your POST body must include:
- JournalBatchNumber (auto-generated or specified)
- JournalName (must exist and be accessible)
- AccountType and LedgerAccount (valid main account)
- TransactionCurrencyCode
- Debit or Credit amount
Common Authentication Issues:
-
401 on POST but GET works: Missing CSRF token - implement the two-step pattern above
-
403 Forbidden: Service principal lacks security role or journal access - verify role assignments and journal name permissions
-
Token expires mid-session: OAuth tokens expire (typically 1 hour) - implement token refresh logic using refresh_token grant
-
CSRF token invalid: Session expired - fetch new CSRF token before retry
Testing Recommendations:
- Use Postman or similar tool to test the authentication flow manually first
- Enable OData logging in D365: System Administration > Setup > Client performance options
- Review authentication failures in Azure AD sign-in logs
- Test with simple journal entry (single line, minimal fields) before complex multi-line entries
The key insight is that D365 OData uses a hybrid security model: OAuth2 for authentication and CSRF tokens for write operation authorization. Both must be present and valid for journal posting to succeed.
Yes, there’s a crucial difference between read and write operations in D365 OData. For POST/PATCH/DELETE operations, you need to include the CSRF token in your request headers. D365 requires this for state-changing operations even with valid OAuth2 authentication. First make a GET request to retrieve the CSRF token from the response headers, then include it as ‘X-CSRF-Token’ in your POST request. The token is typically valid for the session duration.
Good point - I checked and our service principal is mapped to a D365 service account with the Accountant role. That should allow journal posting, right? The confusing part is that GET requests work, which suggests the authentication itself is valid. Is there a separate permission layer for write operations?