Skip to content

Commit 12e177b

Browse files
authored
Merge pull request #1201 from RogerSelwyn/subscription-docs
Docs for subscriptions
2 parents 91a73df + ac4f63a commit 12e177b

File tree

7 files changed

+233
-3
lines changed

7 files changed

+233
-3
lines changed

O365/subscriptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def create_subscription(
116116
) -> Optional[dict]:
117117
"""Create a Microsoft Graph webhook subscription.
118118
119-
See Documentation.md for webhook setup requirements.
119+
See subscriptions usage documentation for webhook setup requirements.
120120
"""
121121
if not notification_url:
122122
raise ValueError("notification_url must be provided.")

docs/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ O365 API
1919
api/onedrive
2020
api/planner
2121
api/sharepoint
22+
api/subscriptions
2223
api/tasks
2324
api/teams
2425
api/utils

docs/source/api/subscriptions.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Subscriptions
2+
-------------
3+
4+
.. include:: global.rst
5+
6+
.. automodule:: O365.subscriptions
7+
:members:
8+
:undoc-members:
9+
:show-inheritance:
10+
:member-order: groupwise

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
# You can specify multiple suffix as a list of string:
5656
#
5757
# source_suffix = ['.rst', '.md']
58-
source_suffix = ".rst"
58+
source_suffix = {".rst": "restructuredtext"}
5959

6060
# The master toctree document.
6161
master_doc = "index"

docs/source/usage.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Detailed Usage
1717
usage/onedrive
1818
usage/planner
1919
usage/sharepoint
20+
usage/subscriptions
2021
usage/tasks
2122
usage/teams
2223
usage/utils
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
Subscriptions
2+
=============
3+
4+
Subscriptions provides the ability to create and manage webhook subscriptions for change notifications against Microsoft Graph. Read here for more details on MS Graph subscriptions
5+
6+
- https://learn.microsoft.com/en-us/graph/api/resources/subscription?view=graph-rest-1.0
7+
- https://learn.microsoft.com/en-us/graph/change-notifications-delivery-webhooks?tabs=http
8+
9+
Create a Subscription
10+
^^^^^^^^^^^^^^^^^^^^^
11+
12+
Assuming a web host (example uses `flask`) and an authenticated account, create a subscription to be notified about new emails.
13+
14+
.. code-block:: python
15+
16+
from flask import Flask, abort, jsonify, request
17+
18+
RESOURCE = "/me/mailFolders('inbox')/messages"
19+
DEFAULT_EXPIRATION_MINUTES = 10069 # Maximum expiration is 10,070 in the future for Outlook message.
20+
21+
app = Flask(__name__)
22+
23+
@app.get("/subscriptions")
24+
def create_subscription():
25+
"""Create a subscription."""
26+
notification_url = request.args.get("notification_url")
27+
if not notification_url:
28+
abort(400, description="notification_url is required")
29+
30+
expiration_minutes = int(request.args.get("expiration_minutes", DEFAULT_EXPIRATION_MINUTES))
31+
client_state = request.args.get("client_state")
32+
resource = request.args.get("resource", RESOURCE)
33+
34+
subscription = account.subscriptions().create_subscription(
35+
notification_url=notification_url,
36+
resource=resource,
37+
change_type="created",
38+
expiration_minutes=expiration_minutes,
39+
client_state=client_state,
40+
)
41+
return jsonify(subscription), 201
42+
43+
@app.post("/webhook")
44+
def webhook_handler():
45+
"""Handle Microsoft Graph webhook calls.
46+
47+
- During subscription validation, Graph sends POST with ?validationToken=... .
48+
We must echo the token as plain text within 10 seconds.
49+
- For change notifications, Graph posts JSON; we just log/ack.
50+
"""
51+
validation_token = request.args.get("validationToken")
52+
if validation_token:
53+
# Echo back token exactly as plain text with HTTP 200.
54+
return validation_token, 200, {"Content-Type": "text/plain"}
55+
56+
# Change notifications: inspect or log as needed.
57+
payload = request.get_json(silent=True) or {}
58+
print("Received notification payload:", payload)
59+
return ("", 202)
60+
61+
Use this url:
62+
63+
``https://<your-tunnel-host>/subscriptions?notification_url=https%3A%2F%2F<your-tunnel-host>%2Fwebhook&client_state=abc123``
64+
65+
HTTP status 201 and the following should be returned:
66+
67+
.. code-block:: JSON
68+
69+
{
70+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
71+
"applicationId": "12345678-bad9-4c34-94d6-f9a1388522f8",
72+
"changeType": "created",
73+
"clientState": "abc123",
74+
"creatorId": "12345678-a5c7-46da-8107-b25090a1ed66",
75+
"encryptionCertificate": null,
76+
"encryptionCertificateId": null,
77+
"expirationDateTime": "2026-01-07T11:20:42.305776Z",
78+
"id": "548355f8-c2c0-47ae-aac7-3ad02b2dfdb1",
79+
"includeResourceData": null,
80+
"latestSupportedTlsVersion": "v1_2",
81+
"lifecycleNotificationUrl": null,
82+
"notificationQueryOptions": null,
83+
"notificationUrl": "https://<your-tunnel-host>/webhook",
84+
"notificationUrlAppId": null,
85+
"resource": "/me/mailFolders('inbox')/messages"
86+
}
87+
88+
List Subscriptions
89+
^^^^^^^^^^^^^^^^^^
90+
91+
.. code-block:: python
92+
93+
@app.get("/subscriptions/list")
94+
def list_subscriptions():
95+
"""List all subscriptions."""
96+
limit = int(request.args.get("limit"))
97+
subscriptions = account.subscriptions().list_subscriptions(limit=limit)
98+
return jsonify(list(subscriptions)), 200
99+
100+
Use this url:
101+
102+
``https://<your-tunnel-host>/subscriptions/list``
103+
104+
HTTP status 200 and the following should be returned:
105+
106+
.. code-block:: JSON
107+
108+
[
109+
{
110+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
111+
"applicationId": "12345678-bad9-4c34-94d6-f9a1388522f8",
112+
"changeType": "created",
113+
"clientState": "abc123",
114+
"creatorId": "12345678-a5c7-46da-8107-b25090a1ed66",
115+
"encryptionCertificate": null,
116+
"encryptionCertificateId": null,
117+
"expirationDateTime": "2026-01-07T11:20:42.305776Z",
118+
"id": "548355f8-c2c0-47ae-aac7-3ad02b2dfdb1",
119+
"includeResourceData": null,
120+
"latestSupportedTlsVersion": "v1_2",
121+
"lifecycleNotificationUrl": null,
122+
"notificationQueryOptions": null,
123+
"notificationUrl": "https://<your-tunnel-host>/webhook",
124+
"notificationUrlAppId": null,
125+
"resource": "/me/mailFolders('inbox')/messages"
126+
}
127+
]
128+
129+
Renew a Subscription
130+
^^^^^^^^^^^^^^^^^^^^
131+
132+
.. code-block:: python
133+
134+
@app.get("/subscriptions/<subscription_id>/renew")
135+
def renew_subscription(subscription_id: str):
136+
"""Renew a subscription."""
137+
expiration_minutes = int(request.args.get("expiration_minutes", DEFAULT_EXPIRATION_MINUTES))
138+
updated = account.subscriptions().renew_subscription(
139+
subscription_id,
140+
expiration_minutes=expiration_minutes,
141+
)
142+
return jsonify(updated), 200
143+
144+
Use this url:
145+
146+
``http://<your-tunnel-host>/subscriptions/548355f8-c2c0-47ae-aac7-3ad02b2dfdb1/renew?expiration_minutes=10069``
147+
148+
HTTP status 200 and the following should be returned:
149+
150+
.. code-block:: JSON
151+
152+
{
153+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
154+
"applicationId": "12345678-bad9-4c34-94d6-f9a1388522f8",
155+
"changeType": "created",
156+
"clientState": "abc123",
157+
"creatorId": "12345678-a5c7-46da-8107-b25090a1ed66",
158+
"encryptionCertificate": null,
159+
"encryptionCertificateId": null,
160+
"expirationDateTime": "2026-01-07T11:35:40.301594Z",
161+
"id": "548355f8-c2c0-47ae-aac7-3ad02b2dfdb1",
162+
"includeResourceData": null,
163+
"latestSupportedTlsVersion": "v1_2",
164+
"lifecycleNotificationUrl": null,
165+
"notificationQueryOptions": null,
166+
"notificationUrl": "https://<your-tunnel-host>/webhook",
167+
"notificationUrlAppId": null,
168+
"resource": "/me/mailFolders('inbox')/messages"
169+
}
170+
171+
Delete a Subscription
172+
^^^^^^^^^^^^^^^^^^^^^
173+
174+
.. code-block:: python
175+
176+
@app.get("/subscriptions/<subscription_id>/delete")
177+
def delete_subscription(subscription_id: str):
178+
"""Delete a subscription."""
179+
deleted = account.subscriptions().delete_subscription(subscription_id)
180+
if not deleted:
181+
abort(404, description="Subscription not found")
182+
return ("", 204)
183+
184+
Use this url:
185+
186+
``http://<your-tunnel-host>/subscriptions/548355f8-c2c0-47ae-aac7-3ad02b2dfdb1/delete``
187+
188+
HTTP status 204 should be returned.
189+
190+
Webhook
191+
^^^^^^^
192+
193+
With a subscription as described above and an email sent to the inbox, a webhook will be received as below:
194+
195+
.. code-block:: python
196+
197+
{
198+
'value': [
199+
{
200+
'subscriptionId': '548355f8-c2c0-47ae-aac7-3ad02b2dfdb12',
201+
'subscriptionExpirationDateTime': '2026-01-07T11:35:40.301594+00:00',
202+
'changeType': 'created',
203+
'resource': 'Users/12345678-a5c7-46da-8107-b25090a1ed66/Messages/<long_guid>=',
204+
'resourceData': {
205+
'@odata.type': '#Microsoft.Graph.Message',
206+
'@odata.id': 'Users/12345678-a5c7-46da-8107-b25090a1ed66/Messages/<long_guid>=',
207+
'@odata.etag': 'W/"CQAAABYACCCoiRErLbiNRJDCFyMjq4khBBnH4N7A"',
208+
'id': '<long_guid>='
209+
},
210+
'clientState': 'abc123',
211+
'tenantId': '12345678-abcd-1234-abcd-1234567890ab'
212+
}
213+
]
214+
}
215+
216+
The client state should be validated for accuracy and if correct, the message can be acted upon as approriate for the type of subscription.
217+
218+
An example application can be found in the examples directory here - https://github.com/O365/python-o365/blob/master/examples/subscriptions_example.py

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ dev = [
3434
"click>=8.1.8",
3535
"pytest>=8.3.4",
3636
"sphinx>=7.4.7",
37-
"sphinx-rtd-theme>=3.0.2",
37+
"sphinx-rtd-theme>=3.0.2"
3838
]
3939

4040
[build-system]

0 commit comments

Comments
 (0)