How-to
Automating Reporting WorkflowsDeep dive

Share an Excel File in OneDrive with a Link Using Graph

Share an Excel workbook stored in OneDrive or SharePoint from Python with Microsoft Graph: create view or edit links scoped to your organisation or to named people, set an expiry date, invite specific recipients with or without an email, list who has access, and remove links that are no longer needed.

Creating a report in OneDrive or SharePoint is half the job; the other half is getting it to the people who need it, with the right level of access and nothing more. Clicking "Share" by hand every month does not scale, and an attachment loses the advantage of a single live file. Microsoft Graph exposes the same sharing model as the OneDrive and SharePoint interfaces: links with a type (view or edit) and a scope (anyone, people in your organisation, or specific people), optional expiry, and direct invitations to named recipients. This page creates each kind from Python, invites recipients without sending Microsoft's notification email so your own report email can carry the link, lists the permissions on a file and removes links that should not outlive the reporting period. It belongs to Automating Excel Online with Microsoft Graph.

Prerequisites

Bash
pip install requests

You need a Microsoft Graph token with Files.ReadWrite (delegated) or Files.ReadWrite.All (application), the drive ID and the item ID of the workbook — for example from the upload in Create a New Workbook in OneDrive with Microsoft Graph. What a link may do is limited by your tenant's sharing policy: if administrators have disabled anonymous links, asking for one fails, whatever the code says. The outputs below come from a mocked Graph API (the responses library) that returns the documented response shapes, so they can be shown without a tenant.

A sharing link has two independent settings, and the combination decides who can do what:

Link type and scope together decide access The type sets what holders can do, view or edit. The scope sets who can use the link — anyone holding it, people signed in to your organisation, or only people explicitly granted access. Organisation view links suit internal reports; anonymous edit links are rarely appropriate. view edit anonymous anyone with the link reads anyone can change it — avoid organization colleagues read — usual choice colleagues edit users only named people read only named people edit

For internal reports, a view link with organization scope is the usual choice: anyone signed in to your Microsoft 365 tenant can open it, nobody outside can, and nobody can change the numbers. anonymous links work for anyone who holds the URL, which makes them easy to forward and impossible to control — use them only for genuinely public data, and always with an expiry. users links work only for people who have been granted access, which suits sensitive reports such as payroll or board packs.

Think about what the recipients will actually do with the file. Most report readers only look: they filter, sort and maybe copy a figure into an email, none of which needs edit rights — Excel for the web lets viewers sort and filter a view of their own without changing the file. Edit access matters for workbooks people are expected to fill in, such as a forecast return or a checklist, and those are better served by inviting named people than by a link anyone in the company can open. Starting from the narrowest access that works and widening it on request is easier than discovering later that a salary file was reachable by the whole tenant.

A link is created with a POST to the item's createLink action. The response is a permission whose link.webUrl is the URL to send:

Python
import os
from datetime import datetime, timedelta, timezone
import requests

GRAPH = "https://graph.microsoft.com/v1.0"
DRIVE, ITEM = "b!reports-drive", "01ITEMSALES0926"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['GRAPH_TOKEN']}"

def create_link(kind="view", scope="organization", expires_in_days=None):
    body = {"type": kind, "scope": scope}
    if expires_in_days:
        expiry = datetime.now(timezone.utc) + timedelta(days=expires_in_days)
        body["expirationDateTime"] = expiry.strftime("%Y-%m-%dT%H:%M:%SZ")
    r = session.post(f"{GRAPH}/drives/{DRIVE}/items/{ITEM}/createLink", json=body, timeout=60)
    r.raise_for_status()
    return r.json()

org_view = create_link()
print(org_view["link"]["scope"], org_view["link"]["type"], org_view["roles"], org_view["link"]["webUrl"])
public = create_link("view", "anonymous", expires_in_days=14)
print(public["link"]["scope"], "expires", public["expirationDateTime"][:10])

createLink is idempotent for the same type and scope: asking for an organisation view link twice returns the existing link rather than making a second one, so a monthly job can call it every time without accumulating links. If each month's report is a separate file, each gets its own link; if the job overwrites one file in place, the link people bookmarked keeps working and always shows the latest figures. expirationDateTime is a UTC timestamp in ISO 8601 format; after it the link stops working. Some settings depend on the account type — password-protected links, for instance, are available only on OneDrive for personal accounts — and your tenant can enforce a maximum expiry for anonymous links regardless of what you ask for.

Step 3 — Invite named people

To give specific people access — and optionally have Microsoft send them a notification — use invite:

Python
def invite(emails, role="read", notify=False, message=None):
    body = {"recipients": [{"email": e} for e in emails], "roles": [role],
            "requireSignIn": True, "sendInvitation": notify}
    if notify and message:
        body["message"] = message
    r = session.post(f"{GRAPH}/drives/{DRIVE}/items/{ITEM}/invite", json=body, timeout=60)
    r.raise_for_status()
    return r.json()["value"]

granted = invite(["maria.lopez@contoso.com", "tom.becker@contoso.com"], role="read")
for p in granted:
    print(p["roles"], p["grantedToV2"]["user"]["displayName"], p["grantedToV2"]["user"]["email"])

requireSignIn: True means the recipients must sign in as themselves to open the file — the link cannot be forwarded to someone else. With sendInvitation: False, Graph grants access silently, which is useful when your own report email already contains the link and a second notification from Microsoft would be noise; set it to True with a message when the sharing email is the notification. Roles are read or write. Recipients outside your organisation are invited as guests if the tenant allows external sharing; their first visit asks them to verify their address with a one-time code, so warn them in your own email that this step is coming. Sending the link in your own email is covered in Send an Excel Report with the Microsoft Graph API, and the trade-offs of links versus attachments in Email a Download Link Instead of an Excel Attachment.

Sharing a monthly report The job uploads the workbook, creates or reuses an organisation view link, grants named recipients read access silently, sends its own email with the link, and at the end of the period removes links and grants that should not remain. upload report driveItem id createLink view, organization invite read, no Microsoft email send own email link in the body tidy up delete old permissions

Step 4 — List and remove permissions

Every link and grant appears as a permission on the item. Listing them shows who has access; deleting one revokes it:

Python
def permissions():
    return session.get(f"{GRAPH}/drives/{DRIVE}/items/{ITEM}/permissions", timeout=60).json()["value"]

def describe(p):
    if "link" in p:
        return f"link {p['link']['type']}/{p['link']['scope']}" + (f" until {p['expirationDateTime'][:10]}" if "expirationDateTime" in p else "")
    return f"{p['roles'][0]} for {p['grantedToV2']['user']['displayName']}"

for p in permissions():
    print(f"{p['id']:<7} {describe(p)}")

for p in permissions():
    if p.get("link", {}).get("scope") == "anonymous":
        session.delete(f"{GRAPH}/drives/{DRIVE}/items/{ITEM}/permissions/{p['id']}", timeout=60).raise_for_status()
print("after removing anonymous links:", [describe(p) for p in permissions()])

The owner permission cannot be removed and inherited permissions from the parent folder or site appear too — leave those alone and delete only the links and grants your job created. A clean-up step like this, run when a reporting period closes, keeps old files from staying reachable through forgotten links. Recording each permission ID as you create it, in a log or a small state file, makes it easy to revoke exactly what the job granted and nothing else; Log Python Excel Script Output to a File covers the logging side.

The same listing doubles as an access audit. A short script that walks a reports folder, lists the permissions on every workbook and flags anonymous links, edit links, links without an expiry and grants to addresses outside your domain gives a security reviewer exactly what they ask for, and it takes a few minutes to run rather than an afternoon of clicking through the "Manage access" panel file by file. Run it monthly and keep its output with the period's reports.

Which sharing method for which report For internal reports anyone in the company may see, use an organisation view link. For sensitive reports, invite named people with sign-in required. For external recipients, use an expiring anonymous view link only if policy allows, otherwise invite them as guests. Who should open the report? anyone in company organization view link reusable each month named people invite, requireSignIn cannot be forwarded external expiring anonymous link or invite as guests

Common pitfalls

  • Anonymous edit links. Anyone holding the URL can change the numbers.
  • No expiry on public links. Set expirationDateTime; policy may cap it anyway.
  • Double notifications. Use sendInvitation: False when your own email has the link.
  • Deleting inherited permissions. Remove only what your job created.
  • Assuming the tenant allows it. Sharing policy overrides the request.

Conclusion

To share a workbook in OneDrive or SharePoint from Python, POST to the item's createLink with a type (view or edit) and scope (anonymous, organization or users), optionally with an expiry, and send the returned link.webUrl. Use invite with requireSignIn to grant named people access, silently or with a notification. List the item's permissions to audit access and delete the links and grants that should not outlive the reporting period.

Frequently asked questions

How do I create a sharing link for a OneDrive file in Python? POST to the createLink action on the drive item with a type of view or edit and a scope of anonymous, organization or users; the response's link.webUrl is the URL to share.

How do I make a sharing link expire? Include expirationDateTime as a UTC ISO 8601 timestamp in the createLink request; the link stops working after that time.

How do I share a file with specific people without sending an email? Use the invite action with their addresses, a role, requireSignIn set to true and sendInvitation set to false.

How do I remove a sharing link with Microsoft Graph? List the item's permissions, find the one for that link and send a DELETE request for that permission ID.

More on excel online & microsoft graph

More on excel online & microsoft graph →