We build custom applications 5x faster and cheaper 🚀
Book a Free Consultation
Stuck on an error? Book a 30-minute call with an engineer and get a direct fix + next steps. No pressure, no commitment.
This is how to integrate Outlook (Microsoft 365 mail via Microsoft Graph) with OpenClaw in a correct, production-ready way: register an Azure AD app and implement the OAuth 2.0 authorization-code flow and refresh/token storage outside the agent; create and renew Microsoft Graph webhook subscriptions (change notifications) to notify your service; implement a secure webhook endpoint that completes Microsoft’s validation handshake and quickly enqueues notification processing; configure your OpenClaw skill in ClawHub with only the non-sensitive configuration (or references to secrets stored in a vault) so the agent can call your secure service endpoints or short-lived REST functions; and keep durable state (refresh tokens, subscription expiry, queues, background jobs) external to the agent runtime. Below I walk the architecture, concrete steps, security and operational practices, debugging checklist, and minimal, real code examples for the OAuth flow, token refresh, Graph calls, and subscription creation.
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id={CLIENT_ID}
&response_type=code
&redirect_uri={REDIRECT_URI}
&response_mode=query
&scope=offline_access%20openid%20profile%20Mail.ReadWrite
&state={CSRF_STATE}
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id={CLIENT_ID}
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&code={AUTHORIZATION_CODE}
&redirect_uri={REDIRECT_URI}
&grant_type=authorization_code
&client_secret={CLIENT_SECRET}
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id={CLIENT_ID}
&grant_type=refresh_token
&refresh_token={REFRESH_TOKEN}
&client_secret={CLIENT_SECRET}
GET https://graph.microsoft.com/v1.0/me/messages
Authorization: Bearer {ACCESS_TOKEN}
Accept: application/json
POST https://graph.microsoft.com/v1.0/subscriptions
Content-Type: application/json
Authorization: Bearer {ACCESS_TOKEN}
{
"changeType": "created,updated",
"notificationUrl": "https://your.example.com/webhook",
"resource": "me/messages",
"expirationDateTime": "2026-03-10T18:23:45.9356913Z",
"clientState": "random-secret-for-verification"
}
const express = require('express');
const fetch = require('node-fetch');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const TENANT = process.env.TENANT_ID;
const REDIRECT_URI = process.env.REDIRECT_URI;
/b> // Exchange code for tokens (OAuth callback) */
app.get('/oauth/callback', async (req, res) => {
const code = req.query.code;
/b> // Exchange code for tokens */
const params = new URLSearchParams();
params.append('client_id', CLIENT_ID);
params.append('scope', 'https://graph.microsoft.com/.default');
params.append('code', code);
params.append('redirect_uri', REDIRECT_URI);
params.append('grant_type', 'authorization_code');
params.append('client_secret', CLIENT_SECRET);
const tokenResp = await fetch(`https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token`, {
method: 'POST',
body: params
});
const tokenJson = await tokenResp.json();
/b> // Persist tokenJson.refresh_token securely in your DB keyed to the user/account */
res.send('OK');
});
/b> // Webhook endpoint: validate and enqueue notifications */
app.post('/webhook', async (req, res) => {
/b> // Microsoft Graph validation: if validation token present, echo it (see Graph docs) */
if (req.query && req.query.validationToken) {
return res.status(200).send(req.query.validationToken);
}
/b> // Validate clientState or other fields, then enqueue for async processing */
const notifications = req.body.value || [];
for (const note of notifications) {
/b> // Enqueue note (resource, subscriptionId, tenantId) to your queue for processing */
}
res.sendStatus(202);
});
/b> // Example background job: fetching a message with stored refresh token */
async function fetchMessageForUser(userId, messageId) {
/b> // 1) Lookup refresh_token for userId from DB */
/b> // 2) Ensure you have a valid access_token (refresh if needed) */
/b> // 3) Call Graph to GET https://graph.microsoft.com/v1.0/users/{userId}/messages/{messageId} */
}
Speak one‑on‑one with a senior engineer about your no‑code app, migration goals, and budget. In just half an hour you’ll leave with clear, actionable next steps—no strings attached.
1
Direct answer: "invalid_client" or "consent_required" means the Outlook OAuth app registration or consent flow is misconfigured — usually wrong client_id/client_secret, mismatched redirect URI, missing admin consent, or using the wrong OAuth flow for how the OpenClaw skill is configured. Fix the app registration and consent, ensure OpenClaw skill uses the exact credentials and callback, and re-run the auth flow.
2
The missing events/attendees usually mean you used the wrong Graph permission type. Delegated tokens act as a user (invitations appear and attendees are populated); app‑only (client credentials) acts as the app and must target a specific mailbox, have the correct Application permissions, and be allowed by an Application Access Policy. Also verify your event payload includes correct attendees.emailAddress.address.
POST https://graph.microsoft.com/v1.0/users/{userId}/events
Authorization: Bearer {token}
Content-Type: application/json
<b>//</b> minimal body with attendees
{
"subject":"Meeting",
"start":{"dateTime":"2026-03-10T10:00:00","timeZone":"UTC"},
"end":{"dateTime":"2026-03-10T11:00:00","timeZone":"UTC"},
"attendees":[{"emailAddress":{"address":"[email protected]","name":"Alice"},"type":"required"}]
}
3
The usual causes are a failed validation handshake (Graph sends a validationToken you must echo as plain text with 200 quickly), an unreachable or non‑TLS public endpoint, incorrect permissions or expired tokens when creating/renewing subscriptions, or renewal calls that fail. Check OpenClaw skill logs, Graph API responses, and subscription objects for expirationDateTime.
const express = require('express')
const app = express()
app.get('/webhook', (req, res) => {
<b>//</b> Graph validation uses query param validationToken
const token = req.query.validationToken
if(token) return res.type('text/plain').status(200).send(token)
res.sendStatus(204)
})
4
Short answer: handle Graph 429s with proper rate‑limit handling (read Retry‑After and Graph headers), use exponential backoff with jitter, and make operations idempotent by deduplicating retries via client-side operation IDs stored outside the agent (DB) or using stable resource IDs/ETags; move retry and state logic out of transient OpenClaw agents into a hosted worker.
const opId = generateId();
// // persist opId before calling Graph
await db.save(opId,{status:'pending'});
const res = await fetch(url,{method:'POST',headers:{'Idempotency-Key':opId}});
if(res.status===429){await sleep(retryAfter*1000); /* retry using opId */}This prompt helps an AI assistant understand your setup and guide you through the fix step by step, without assuming technical knowledge.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â