# Microsoft 365

Connect a Microsoft 365 or Outlook.com account to use Outlook mail, Outlook Calendar with Teams meetings, OneDrive files and Outlook contacts.

One Microsoft connection gives Pozy a person's Outlook mail, Outlook Calendar, OneDrive and Outlook contacts. It works for Microsoft 365 work or school logins and for personal Outlook.com or Hotmail logins alike. Each account stands for one mailbox, and you pick the areas it covers when you connect. The calls are shared with Google mailboxes: see [Email](https://pozy.ai/docs/productivity/email), [Calendar](https://pozy.ai/docs/productivity/calendar) and [Files](https://pozy.ai/docs/productivity/files).

## What you can do

| Area | Value in `scopes` | Microsoft Graph permissions | What it unlocks |
| --- | --- | --- | --- |
| Email | `email` | `Mail.ReadWrite`, `Mail.Send` | List, search, read, send, reply, flag, move and trash Outlook mail |
| Calendar | `calendar` | `Calendars.ReadWrite` | Events with Teams meetings, RSVP, free-busy and availability |
| Files | `files` | `Files.ReadWrite` | Every OneDrive file the user can reach, no picker needed |
| Contacts | `contacts` | `Contacts.Read` | Outlook contacts copied into `/v1/contacts` once a day, read-only |

Each connection adds `openid`, `profile`, `email`, `offline_access` and `User.Read` as well: they identify the mailbox and let Pozy keep access over time.

| Property | Value |
| --- | --- |
| Platform id | `microsoft` |
| Mailboxes | Exchange Online and Outlook.com. On-premises Exchange is not supported |
| Organizations | Some tenants need an administrator to approve the app once ([admin consent](#admin-consent)) |
| Inbox | New mail and API sends appear as inbox conversations, one per Outlook conversation |
| List the accounts | `GET /v1/accounts?platform=microsoft` |

## Connect an account

```bash cURL
curl "https://api.pozy.ai/v1/connect/microsoft?profileId=6710c3a9e4b0d2f1a8c95e21&scopes=email,calendar&redirect_url=https://app.example.com/connected" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/microsoft?profileId=6710c3a9e4b0d2f1a8c95e21&scopes=email,calendar&redirect_url=https:%2F%2Fapp.example.com%2Fconnected", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/connect/microsoft",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={
        "profileId": "6710c3a9e4b0d2f1a8c95e21",
        "scopes": "email,calendar",
        "redirect_url": "https://app.example.com/connected",
    },
)
```

The user signs in, approves the permissions behind the areas, and returns to your `redirect_url` with `connected=microsoft`, `profileId`, `accountId` and `username`. Pozy reads the granted areas from the token Microsoft returns.

To add areas later, connect the same account again with the longer list and `reconnectAccountId`. The account id stays the same. `loginHint` preselects the mailbox in Microsoft's account picker. Areas only add up; to drop one, disconnect and connect again.

## Admin consent

Many work and school tenants let only an administrator approve apps that read mail, calendars, files or contacts. Users then see "Need admin approval" while connecting, or later calls return `412 admin_consent_required` with a ready-made link in `details.adminConsentUrl` (valid for one hour). Personal Outlook.com accounts never need this.

Request a fresh link for an account in that organization and send it to an administrator (Global Administrator, Cloud Application Administrator or Application Administrator):

```bash cURL
curl "https://api.pozy.ai/v1/connect/microsoft/admin-consent?accountId=6710c50ce4b0d2f1a8c95e82&redirectUrl=https://app.example.com/admin-done" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/microsoft/admin-consent?accountId=6710c50ce4b0d2f1a8c95e82&redirectUrl=https:%2F%2Fapp.example.com%2Fadmin-done", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.data.adminConsentUrl);
```
```python Python
resp = requests.get(
    "https://api.pozy.ai/v1/connect/microsoft/admin-consent",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"accountId": "6710c50ce4b0d2f1a8c95e82", "redirectUrl": "https://app.example.com/admin-done"},
)
print(resp.json()["data"]["adminConsentUrl"])
```

The administrator signs in, reviews the permissions and approves them for the whole tenant. Microsoft sends them to `redirectUrl` with `adminConsent=granted` and `tenant`, or `adminConsent=denied` and `error`. Calls that returned `412` then work without a reconnect. An administrator can also approve while connecting their own mailbox by ticking "Consent on behalf of your organization".

Approval is per organization and covers all four areas. Each user still chooses what their own connection grants. An administrator can withdraw it in Microsoft Entra under Enterprise applications, after which accounts in that tenant fail until it is approved again.

## Outlook limits

| Limit | Value |
| --- | --- |
| Sends a day | 1,000 per mailbox, reset at midnight UTC |
| Same body to more than 50 recipients within an hour | The mailbox pauses: 10 minutes, doubling up to 24 hours, and `account.rate_limited` fires |
| Addresses per field | 100 in `to`, 100 in `cc`, 100 in `bcc` |
| Attachments | 20 files, none over 3 MB, 25 MB combined |
| Custom headers | Only names that start with `X-`. A `List-Unsubscribe` header is removed, and `metadata.droppedHeaders` on the result says so |

Each send response includes the `X-Pozy-Send-Limit`, `X-Pozy-Send-Remaining` and `X-Pozy-Send-Reset` headers. For repeating events Outlook takes a single RRULE, with no `EXDATE` or `RDATE`, and it stores just one reminder per event. Upload conversion with `convertTo` and picker sessions belong to Google; on Microsoft they answer `400`.

## Push notifications

Pozy creates Microsoft Graph subscriptions for the granted areas and renews them before they expire (about three days for mail and calendar, about 29 days for OneDrive). Subscribe your own [webhook](https://pozy.ai/docs/webhooks) to the events you need:

| Event | Area | Fires when |
| --- | --- | --- |
| `message.received`, `message.sent`, `conversation.started` | `email` | Mail arrives in the Inbox folder, is sent through the API, or starts a thread |
| `calendar.event.created`, `.updated`, `.deleted` | `calendar` | An event in the user's calendar is added, edited or removed |
| `file.created`, `file.updated`, `file.deleted` | `files` | A file in OneDrive is added, edited or removed |
| `contact.updated`, `contact.deleted` | `contacts` | The once-a-day contact sync finds a changed or removed contact |
| `account.rate_limited` | `email` | Sending from the mailbox is paused for a while |

If the push subscription is down, mail received in that time produces no webhooks; catch up with `GET /v1/email/messages`. The subscription's state is the `push` field of `GET /v1/accounts/{accountId}/health`.

## Errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400 invalid_field_value` | `scopes` missing, or a Google-only option such as `google_meet` | Fix the field named in `param` |
| `error=oauth_denied` on the redirect | The user said no, or their organization does not allow the app | Try again, or ask for [admin consent](#admin-consent) |
| `401 reconnect_required` | Microsoft no longer accepts the account's access | Have the user reconnect |
| `412 admin_consent_required` | The tenant needs an administrator's approval | Send `details.adminConsentUrl` to an administrator |
| `412 account_scope_missing` | The account lacks the area | Reconnect with the area added |
| `429 email_send_cap_reached`, `429 rate_limited` | Daily cap used, cooldown, or Graph throttling | Wait for `resetsAt` or `Retry-After` |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/{platform}`](https://pozy.ai/docs/api/getConnectUrl) | Get OAuth connect URL |
| [`GET /v1/connect/microsoft/admin-consent`](https://pozy.ai/docs/api/startMicrosoftAdminConsent) | Admin-consent link for a Microsoft tenant |
| [`GET /v1/accounts/{accountId}/health`](https://pozy.ai/docs/api/getAccountHealth) | Check account health |

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/calendar/calendars`](https://pozy.ai/docs/api/listCalendars) | List calendars |
| [`GET /v1/calendar/events`](https://pozy.ai/docs/api/listCalendarEvents) | List events in a window |
| [`POST /v1/calendar/events`](https://pozy.ai/docs/api/createCalendarEvent) | Create an event |
| [`GET /v1/calendar/events/{eventId}`](https://pozy.ai/docs/api/getCalendarEvent) | Get an event |
| [`PATCH /v1/calendar/events/{eventId}`](https://pozy.ai/docs/api/updateCalendarEvent) | Update an event |
| [`DELETE /v1/calendar/events/{eventId}`](https://pozy.ai/docs/api/deleteCalendarEvent) | Delete an event |
| [`POST /v1/calendar/events/{eventId}/rsvp`](https://pozy.ai/docs/api/rsvpCalendarEvent) | Respond to an invitation |
| [`POST /v1/calendar/free-busy`](https://pozy.ai/docs/api/calendarFreeBusy) | Busy blocks for one or more accounts |
| [`POST /v1/calendar/availability`](https://pozy.ai/docs/api/calendarAvailability) | Find meeting slots across accounts |
| [`GET /v1/email/threads`](https://pozy.ai/docs/api/listEmailThreads) | List email threads |
| [`GET /v1/email/threads/{threadId}`](https://pozy.ai/docs/api/getEmailThread) | Get an email thread with its messages |
| [`PATCH /v1/email/threads/{threadId}`](https://pozy.ai/docs/api/updateEmailThread) | Update every message in a thread |
| [`GET /v1/email/messages`](https://pozy.ai/docs/api/listEmailMessages) | List email messages |
| [`POST /v1/email/messages`](https://pozy.ai/docs/api/sendEmailMessage) | Send an email |
| [`GET /v1/email/messages/{messageId}`](https://pozy.ai/docs/api/getEmailMessage) | Get an email message |
| [`PATCH /v1/email/messages/{messageId}`](https://pozy.ai/docs/api/updateEmailMessage) | Mark read or starred, or move a message |
| [`DELETE /v1/email/messages/{messageId}`](https://pozy.ai/docs/api/trashEmailMessage) | Move a message to trash |
| [`POST /v1/email/messages/{messageId}/reply`](https://pozy.ai/docs/api/replyEmailMessage) | Reply to a message |
| [`GET /v1/email/folders`](https://pozy.ai/docs/api/listEmailFolders) | List mailbox folders |
| [`GET /v1/email/attachments/{attachmentId}`](https://pozy.ai/docs/api/downloadEmailAttachment) | Download an attachment |
| [`GET /v1/files`](https://pozy.ai/docs/api/listFiles) | List files in a folder or search the drive |
| [`POST /v1/files`](https://pozy.ai/docs/api/uploadFile) | Upload a file |
| [`POST /v1/files/folders`](https://pozy.ai/docs/api/createFolder) | Create a folder |
| [`POST /v1/files/picker-sessions`](https://pozy.ai/docs/api/createFilePickerSession) | Create a hosted Google Picker session (not available yet) |
| [`GET /v1/files/{fileId}`](https://pozy.ai/docs/api/getFile) | Get a file or folder |
| [`PATCH /v1/files/{fileId}`](https://pozy.ai/docs/api/updateFile) | Rename or move a file |
| [`DELETE /v1/files/{fileId}`](https://pozy.ai/docs/api/trashFile) | Move a file to the trash |
| [`GET /v1/files/{fileId}/download`](https://pozy.ai/docs/api/downloadFile) | Download a file |
| [`GET /v1/files/{fileId}/share-link`](https://pozy.ai/docs/api/getFileShareLink) | Get an anyone-with-the-link share link |
