Production SDK patterns for OneNote Graph API: retry logic, batch requests, and safe file uploads.
Use when building production OneNote integrations that need rate limit handling and reliable uploads.
Trigger with "onenote sdk patterns", "onenote retry logic", "onenote batch requests".
Production SDK patterns for OneNote Graph API: retry logic, batch requests, and safe file uploads.
Use when building production OneNote integrations that need rate limit handling and reliable uploads.
Trigger with "onenote sdk patterns", "onenote retry logic", "onenote batch requests".
allowed-tools
Read, Write, Edit, Bash(npm:*), Bash(pip:*), Grep
version
1.6.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
["saas","onenote","microsoft"]
compatibility
Designed for Claude Code
OneNote SDK Patterns
Overview
Production-grade patterns for the OneNote Graph API. The two biggest production issues are rate limits (600 requests per 60 seconds per user, 10,000 per 10 minutes per tenant) and silent upload failures where files >4MB return 200 OK with an empty response body — the page is never created but no error is raised.
This skill provides middleware chains, retry decorators, batch request patterns, and silent failure detection for both TypeScript and Python.
Prerequisites
Completed onenote-install-auth — working Graph API authentication
Understanding of async/await patterns in your target language
Node.js 18+ or Python 3.10+
Instructions
Pattern 1: Retry Middleware with Retry-After Header Parsing (TypeScript)
The Graph API returns a Retry-After header (in seconds) with 429 responses. Hardcoding a fixed retry delay wastes time or hits limits again.
Reduce round trips by batching up to 20 Graph requests into a single HTTP call.
// Batch request to fetch multiple pages in parallelconst batchBody = {
requests: [
{ id: "1", method: "GET", url: `/me/onenote/pages/${pageId1}` },
{ id: "2", method: "GET", url: `/me/onenote/pages/${pageId2}` },
{ id: "3", method: "GET", url: `/me/onenote/pages/${pageId3}` },
],
};
const batchResponse = await client
.api("/$batch")
.post(batchBody);
// Each response has its own status code — some may fail while others succeedfor (const resp of batchResponse.responses) {
if (resp.status === 200) {
console.log(`Page ${resp.id}: ${resp.body.title}`);
} else {
console.error(`Page ${resp.id} failed: ${resp.status} — ${resp.body?.error?.message}`);
}
}
Batch limits: Maximum 20 requests per batch. Requests within a batch count individually toward rate limits. Use dependsOn for sequential ordering within a batch.
Pattern 5: Safe File Upload with Silent Failure Detection
Files larger than 4MB return 200 OK with an empty response body. The page is never created. You must check the response body after every upload.
constMAX_UPLOAD_SIZE = 4 * 1024 * 1024; // 4MBasyncfunctionsafeCreatePage(client: Client,
sectionId: string,
htmlContent: string,
attachments?: { name: string; contentType: string; data: Buffer }[]
): Promise<{ success: boolean; pageId?: string; error?: string }> {
// Calculate total payload sizeconst htmlSize = Buffer.byteLength(htmlContent, "utf-8");
const attachmentSize = attachments?.reduce((sum, a) => sum + a.data.length, 0) ?? 0;
const totalSize = htmlSize + attachmentSize;
if (totalSize > MAX_UPLOAD_SIZE) {
return {
success: false,
error: `Payload ${(totalSize / 1024 / 1024).toFixed(1)}MB exceeds 4MB limit. ` +
`Split content or upload images as URLs instead of inline data.`,
};
}
const response = await client
.api(`/me/onenote/sections/${sectionId}/pages`)
.header("Content-Type", "text/html")
.post(htmlContent);
// CRITICAL: Check for silent failure — 200 with empty bodyif (!response || !response.id) {
return {
success: false,
error: "Silent upload failure: API returned 200 but response body is empty. " +
"This typically means the content was too large or contained invalid binary data.",
};
}
return { success: true, pageId: response.id };
}
Pattern 6: Token Refresh Middleware
import { DeviceCodeCredential } from"@azure/identity";
classTokenRefreshMiddleware {
privatecredential: DeviceCodeCredential;
privatecachedToken: string | null = null;
privatetokenExpiry: number = 0;
constructor(credential: DeviceCodeCredential) {
this.credential = credential;
}
asyncgetValidToken(scopes: string[]): Promise<string> {
const now = Date.now();
// Refresh 5 minutes before expiry to avoid mid-request failuresconstREFRESH_BUFFER = 5 * 60 * 1000;
if (this.cachedToken && this.tokenExpiry > now + REFRESH_BUFFER) {
returnthis.cachedToken;
}
const tokenResponse = awaitthis.credential.getToken(scopes);
if (!tokenResponse) {
thrownewError("Failed to acquire token — user may need to re-authenticate");
}
this.cachedToken = tokenResponse.token;
this.tokenExpiry = tokenResponse.expiresOnTimestamp;
returnthis.cachedToken;
}
}