Skip to main content

Connect the Claude M365 plugin to DIAL

In this tutorial you put the Claude for Microsoft 365 add-ins (Excel, PowerPoint, Word, and Outlook) in front of DIAL through an NGINX reverse proxy, and watch a DIAL-hosted model reply inside Office. The add-in speaks the native Anthropic Messages API and DIAL exposes that same API at /anthropic, but a thin proxy bridges three gaps between them. This tutorial is for developers who already have a DIAL host.

Note

This page is a companion to Anthropic's Use Claude for M365 with third-party platforms, which documents the add-in's LLM gateway connection path and the tenant-wide deployment done by your IT team. That page tells you how to deploy and point the add-in at a gateway; this page is the gateway — an NGINX reverse proxy that makes DIAL a valid target for it.

Why a proxy is needed

The Claude M365 add-in can connect to any LLM gateway that speaks the Anthropic Messages API. DIAL speaks it at /anthropic, but the add-in expects three things DIAL's surface does not provide directly:

  • CORS — the add-in's task pane loads from https://pivot.claude.ai, so every request to your gateway is cross-origin. The gateway must return CORS headers on every response, including errors and the OPTIONS preflight.
  • Auth translation — the add-in sends the API key in the x-api-key header. DIAL authenticates on the Api-Key header instead.
  • A GET /v1/models endpoint — the add-in probes it on login to list models. DIAL's Anthropic-compatible surface does not implement it.

The NGINX proxy in this tutorial handles all three. NGINX is one option — any reverse proxy that can do the same three things works just as well (see Good to know).

Prerequisites

  • A DIAL host and API key. See Before you begin.
  • Docker installed, to run NGINX locally.
  • The Claude for Microsoft 365 add-in installed in an Office app. See Anthropic's deployment guide.
  • A way to give a local port a public HTTPS URL. This tutorial uses Cloudflare Tunnel; any equivalent works (see Step 4).

What you'll build

An NGINX reverse proxy, running locally in Docker and exposed over public HTTPS, that the Claude M365 add-in calls as its gateway. Every turn is answered by your DIAL host, with DIAL's roles, quotas, and cost tracking applied.

Claude M365 add-in  --https-->  public HTTPS URL  --http-->  NGINX (Docker, local)  --https-->  DIAL /anthropic (<YOUR_DIAL_HOST>)

Project structure

By the end of this tutorial, your project will look like this:

claude-m365-dial-proxy/
└── nginx.conf

Step 1: Create claude-m365-dial-proxy/nginx.conf

Create the directory and the config file:

mkdir claude-m365-dial-proxy && cd claude-m365-dial-proxy

Create claude-m365-dial-proxy/nginx.conf with the following content:

events {}

http {
upstream dial_backend {
server <YOUR_DIAL_HOST>:443;
}

server {
listen 80;

location = /anthropic/v1/models {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary Origin always;

if ($request_method = OPTIONS) {
return 204;
}

default_type application/json;
# return 200 '{"data":[{"id":"anthropic.claude-sonnet-4-6","object":"model","display_name":"Claude Sonnet 4.6"}],"object":"list","has_more":false}';
return 200 '{"data": [],"object": "list","has_more": false}';
}

location /anthropic/v1/ {
# Strip any CORS headers DIAL/upstream sets, so only ours below applies
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Vary;

add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary Origin always;

if ($request_method = OPTIONS) {
return 204;
}

set $apikey $http_x_api_key;

proxy_pass https://dial_backend/anthropic/v1/;

proxy_ssl_server_name on;
proxy_ssl_name <YOUR_DIAL_HOST>;
proxy_set_header Host <YOUR_DIAL_HOST>;

proxy_set_header Api-Key $apikey;
proxy_set_header X-Api-Key "";

proxy_set_header anthropic-version $http_anthropic_version;
proxy_set_header X-DIAL-CACHE-POLICY $http_x_dial_cache_policy;

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

location / {
return 404;
}
}
}

Warning

Replace <YOUR_DIAL_HOST> with a valid, reachable DIAL backend host — the host part of your DIAL URL, without the scheme or a trailing path (for example, dial.example.com). It appears three times: the upstream server line, proxy_ssl_name, and the Host header. dial_backend is just NGINX's internal name for that upstream and is not a host — leave it as is.

What each part does:

  • Path scoping — only /anthropic/v1/ is proxied; the final location / returns 404 for everything else.
  • CORS — the proxy strips any CORS headers DIAL sets and adds its own, reflecting the caller's origin ($http_origin) and requested headers ($http_access_control_request_headers), and answers the OPTIONS preflight with 204. This is what satisfies the add-in's https://pivot.claude.ai origin.
  • Auth translation — it reads the incoming x-api-key into $apikey, forwards it to DIAL as Api-Key, and blanks X-Api-Key so it is not double-sent upstream.
  • /v1/models stub — the exact location = /anthropic/v1/models block returns a static JSON list instead of proxying to DIAL, which has no such endpoint. It ships returning an empty list; a commented alternative returns a hardcoded model list (see Good to know).
  • Streaming-friendly proxyingproxy_buffering off, proxy_cache off, and HTTP/1.1 with Connection cleared let streamed (SSE) responses pass through unbuffered.

Step 2: Run NGINX in Docker

From inside claude-m365-dial-proxy/, run:

docker run --rm -p 8080:80 \
-v "$(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro" \
nginx:1.27-alpine

This mounts your config read-only and publishes the proxy on port 8080.

Verify:

The container starts and stays running, with no configuration errors in its output.

Step 3: Verify the proxy locally

Send a message through the proxy, replacing the model id and key with your own:

curl -v http://0.0.0.0:8080/anthropic/v1/messages \
-H "x-api-key: <DIAL_API_KEY>" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic.claude-sonnet-4-6","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}'

Verify:

You get a 200 response with a JSON completion from your DIAL deployment. The proxy translated x-api-key to Api-Key and forwarded the request to DIAL's /anthropic surface.

Step 4: Expose the proxy over public HTTPS

The add-in cannot reach 0.0.0.0:8080 — it needs a public HTTPS URL. This requirement comes from the add-in's architecture, not from DIAL: the Office add-in runs inside a sandboxed browser frame hosted by Microsoft 365 and served from https://pivot.claude.ai, so it can only call a publicly reachable HTTPS endpoint. Any DIAL host can sit behind such an endpoint; the proxy just needs a public front door.

This tutorial uses Cloudflare Tunnel, which exposes the local port without opening inbound firewall ports:

cloudflared tunnel --url http://0.0.0.0:8080

It prints a public HTTPS URL such as https://<random>.trycloudflare.com. Keep it — you enter it in Step 5.

Note

Cloudflare Tunnel is one option, not a requirement. Any method that gives your local port a public HTTPS URL works — ngrok, a cloud load balancer, or deploying the same NGINX config on a public host. Pick whatever fits your environment; the rest of this tutorial only needs the resulting URL.

Verify:

Repeat the Step 3 curl against the public URL (https://<random>.trycloudflare.com/anthropic/v1/messages). You get the same 200 completion.

Step 5: Point the Claude M365 add-in at the gateway

Open the add-in in an Office app and, on the sign-in screen, choose Cloud provider or gateway, then Gateway. Enter:

SettingValue
Gateway URLhttps://<random>.trycloudflare.com/anthropic
API token<DIAL_API_KEY>

Use the base URL ending in /anthropic — the add-in appends /v1/messages and /v1/models itself, which is exactly what the proxy scopes. Leave the auth scheme at its default: the add-in sends the token as x-api-key, and the proxy translates it to the Api-Key header DIAL reads.

Verify:

The add-in's connection test succeeds. Open the task pane, send a message such as "What model are you?", and you get a reply answered by your DIAL deployment.

What you learned

  • Why the Claude M365 add-in needs a gateway in front of DIAL: CORS, x-api-key to Api-Key translation, and a stubbed /v1/models endpoint.
  • How to run that gateway as an NGINX reverse proxy in Docker and verify it with curl.
  • Why the gateway must be publicly reachable over HTTPS, and how to expose it.
  • How to configure the add-in's gateway connection so DIAL answers every turn.

Good to know

  • The /v1/models limitation. DIAL's Anthropic-compatible surface has no /v1/models endpoint, so the proxy stubs it. The active variant returns an empty list — the add-in loads, but the model picker is empty and the user enters a model id manually. The commented variant returns a hardcoded list, which gives a working dropdown but silently goes stale when DIAL adds, renames, or retires models, so whoever owns the proxy must keep it in sync. There is no way to get a live model list through this path until DIAL adds native /v1/models support.
  • NGINX is replaceable. Any reverse proxy works — the principle is what matters: reflect the add-in's CORS origin on every response including the preflight, translate x-api-key to Api-Key, stub /v1/models, and pass streamed responses through unbuffered.
  • Future DIAL may not need this. Future DIAL versions may support the Claude M365 plugin integration natively, without an additional reverse proxy. Check your DIAL release notes before building this out for the long term.
  • CORS origin. The add-in's task pane loads from https://pivot.claude.ai; the reflected $http_origin config covers it without hardcoding the origin.

Next steps