chub-dev 0.1.0 → 0.1.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +55 -0
  2. package/bin/chub-mcp +2 -0
  3. package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
  4. package/dist/airtable/docs/database/python/DOC.md +1735 -0
  5. package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
  6. package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
  7. package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
  8. package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
  9. package/dist/asana/docs/tasks/DOC.md +1396 -0
  10. package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
  11. package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
  12. package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
  13. package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
  14. package/dist/auth0/docs/identity/python/DOC.md +1199 -0
  15. package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
  16. package/dist/aws/docs/s3/python/DOC.md +1807 -0
  17. package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
  18. package/dist/binance/docs/trading/python/DOC.md +1454 -0
  19. package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
  20. package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
  21. package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
  22. package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
  23. package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
  24. package/dist/clerk/docs/auth/python/DOC.md +274 -0
  25. package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
  26. package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
  27. package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
  28. package/dist/cohere/docs/llm/DOC.md +1335 -0
  29. package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
  30. package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
  31. package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
  32. package/dist/deepgram/docs/speech/python/DOC.md +685 -0
  33. package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
  34. package/dist/deepl/docs/translation/python/DOC.md +944 -0
  35. package/dist/deepseek/docs/llm/DOC.md +1220 -0
  36. package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
  37. package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
  38. package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
  39. package/dist/discord/docs/bot/python/DOC.md +1130 -0
  40. package/dist/elasticsearch/docs/search/DOC.md +1634 -0
  41. package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
  42. package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
  43. package/dist/firebase/docs/auth/DOC.md +1015 -0
  44. package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
  45. package/dist/gemini/docs/genai/python/DOC.md +555 -0
  46. package/dist/github/docs/octokit/DOC.md +1560 -0
  47. package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
  48. package/dist/google/docs/bigquery/python/DOC.md +1503 -0
  49. package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
  50. package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
  51. package/dist/huggingface/docs/transformers/DOC.md +948 -0
  52. package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
  53. package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
  54. package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
  55. package/dist/jira/docs/issues/python/DOC.md +1492 -0
  56. package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
  57. package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
  58. package/dist/landingai-ade/docs/api/DOC.md +620 -0
  59. package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
  60. package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
  61. package/dist/landingai-ade/skills/SKILL.md +489 -0
  62. package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
  63. package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
  64. package/dist/linear/docs/tracker/DOC.md +1554 -0
  65. package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
  66. package/dist/livekit/docs/realtime/python/DOC.md +163 -0
  67. package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
  68. package/dist/meilisearch/docs/search/DOC.md +1241 -0
  69. package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
  70. package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
  71. package/dist/mongodb/docs/atlas/DOC.md +2041 -0
  72. package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
  73. package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
  74. package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
  75. package/dist/okta/docs/identity/python/DOC.md +1401 -0
  76. package/dist/openai/docs/chat/javascript/DOC.md +407 -0
  77. package/dist/openai/docs/chat/python/DOC.md +568 -0
  78. package/dist/paypal/docs/checkout/DOC.md +278 -0
  79. package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
  80. package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
  81. package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
  82. package/dist/plaid/docs/banking/python/DOC.md +1203 -0
  83. package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
  84. package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
  85. package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
  86. package/dist/prisma/docs/orm/python/DOC.md +1317 -0
  87. package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
  88. package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
  89. package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
  90. package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
  91. package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
  92. package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
  93. package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
  94. package/dist/redis/docs/key-value/python/DOC.md +2054 -0
  95. package/dist/registry.json +2817 -0
  96. package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
  97. package/dist/resend/docs/email/DOC.md +1271 -0
  98. package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
  99. package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
  100. package/dist/search-index.json +1 -0
  101. package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
  102. package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
  103. package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
  104. package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
  105. package/dist/shopify/docs/storefront/DOC.md +457 -0
  106. package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
  107. package/dist/slack/docs/workspace/python/DOC.md +271 -0
  108. package/dist/square/docs/payments/javascript/DOC.md +1855 -0
  109. package/dist/square/docs/payments/python/DOC.md +1728 -0
  110. package/dist/stripe/docs/api/DOC.md +1727 -0
  111. package/dist/stripe/docs/payments/DOC.md +1726 -0
  112. package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
  113. package/dist/stytch/docs/auth/python/DOC.md +1962 -0
  114. package/dist/supabase/docs/client/DOC.md +1606 -0
  115. package/dist/twilio/docs/messaging/python/DOC.md +469 -0
  116. package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
  117. package/dist/vercel/docs/platform/DOC.md +1940 -0
  118. package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
  119. package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
  120. package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
  121. package/dist/zendesk/docs/support/python/DOC.md +2297 -0
  122. package/package.json +22 -6
  123. package/skills/get-api-docs/SKILL.md +84 -0
  124. package/src/commands/annotate.js +83 -0
  125. package/src/commands/build.js +12 -1
  126. package/src/commands/feedback.js +150 -0
  127. package/src/commands/get.js +83 -42
  128. package/src/commands/search.js +7 -0
  129. package/src/index.js +43 -17
  130. package/src/lib/analytics.js +90 -0
  131. package/src/lib/annotations.js +57 -0
  132. package/src/lib/bm25.js +170 -0
  133. package/src/lib/cache.js +69 -6
  134. package/src/lib/config.js +8 -3
  135. package/src/lib/identity.js +99 -0
  136. package/src/lib/registry.js +103 -20
  137. package/src/lib/telemetry.js +86 -0
  138. package/src/mcp/server.js +177 -0
  139. package/src/mcp/tools.js +251 -0
@@ -0,0 +1,1962 @@
1
+ ---
2
+ name: auth
3
+ description: "Stytch Python SDK authentication guide for passwordless and OTP-based authentication"
4
+ metadata:
5
+ languages: "python"
6
+ versions: "13.28.0"
7
+ updated-on: "2026-03-02"
8
+ source: maintainer
9
+ tags: "stytch,auth,authentication,passwordless,otp"
10
+ ---
11
+
12
+ # Stytch Python SDK - Authentication Guide
13
+
14
+ ## Golden Rule
15
+
16
+ **ALWAYS use the official `stytch` package from PyPI.**
17
+
18
+ ```bash
19
+ pip install stytch
20
+ ```
21
+
22
+ **Current version: 13.28.0**
23
+
24
+ **DO NOT use:**
25
+ - Any unofficial or outdated Stytch packages
26
+ - Frontend JavaScript packages when building backend services
27
+
28
+ The `stytch` package is the official backend SDK for Python applications. It supports Python 3.8+ and includes full async/await support.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ### Install the SDK
35
+
36
+ ```bash
37
+ pip install stytch
38
+ ```
39
+
40
+ or with poetry:
41
+
42
+ ```bash
43
+ poetry add stytch
44
+ ```
45
+
46
+ or with uv:
47
+
48
+ ```bash
49
+ uv pip install stytch
50
+ ```
51
+
52
+ ### Environment Variables
53
+
54
+ Set up your Stytch credentials in your environment:
55
+
56
+ ```bash
57
+ STYTCH_PROJECT_ID=project-live-your-project-id
58
+ STYTCH_SECRET=secret-live-your-secret-key
59
+ ```
60
+
61
+ **Get credentials from:** [Stytch Dashboard](https://stytch.com/dashboard)
62
+
63
+ For testing, use test environment credentials:
64
+
65
+ ```bash
66
+ STYTCH_PROJECT_ID=project-test-your-project-id
67
+ STYTCH_SECRET=secret-test-your-secret-key
68
+ ```
69
+
70
+ ### .env File Example
71
+
72
+ ```env
73
+ STYTCH_PROJECT_ID=project-live-c60c0abe-c25a-4472-a9ed-320c6667d317
74
+ STYTCH_SECRET=secret-live-80JASucyk7z_G8Z-7dVwZVGXL5NT_qGAQ2I=
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Initialization
80
+
81
+ ### Basic B2C Client
82
+
83
+ ```python
84
+ import stytch
85
+ import os
86
+
87
+ client = stytch.Client(
88
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
89
+ secret=os.getenv("STYTCH_SECRET"),
90
+ )
91
+ ```
92
+
93
+ ### Client with Environment Override
94
+
95
+ ```python
96
+ client = stytch.Client(
97
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
98
+ secret=os.getenv("STYTCH_SECRET"),
99
+ environment="test", # or "live" (default)
100
+ )
101
+ ```
102
+
103
+ ### B2B Client (for organizations)
104
+
105
+ ```python
106
+ import stytch
107
+
108
+ b2b_client = stytch.B2BClient(
109
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
110
+ secret=os.getenv("STYTCH_SECRET"),
111
+ )
112
+ ```
113
+
114
+ ### Client with Custom Base URL
115
+
116
+ ```python
117
+ client = stytch.Client(
118
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
119
+ secret=os.getenv("STYTCH_SECRET"),
120
+ custom_base_url="https://api.custom-domain.com/",
121
+ )
122
+ ```
123
+
124
+ ### Async Client Usage
125
+
126
+ All methods have async versions by appending `_async`:
127
+
128
+ ```python
129
+ import asyncio
130
+
131
+ async def main():
132
+ client = stytch.Client(
133
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
134
+ secret=os.getenv("STYTCH_SECRET"),
135
+ )
136
+
137
+ response = await client.magic_links.email.login_or_create_async(
138
+ email="user@example.com"
139
+ )
140
+ print(response.user_id)
141
+
142
+ asyncio.run(main())
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Core API Surfaces
148
+
149
+ ### 1. Magic Links
150
+
151
+ Magic links provide passwordless authentication via email.
152
+
153
+ #### Send Magic Link (Login or Create)
154
+
155
+ ```python
156
+ # Minimal example
157
+ response = client.magic_links.email.login_or_create(
158
+ email="user@example.com",
159
+ )
160
+
161
+ print(response.user_id)
162
+ print(response.email_id)
163
+ ```
164
+
165
+ #### Send Magic Link with Custom URLs
166
+
167
+ ```python
168
+ response = client.magic_links.email.login_or_create(
169
+ email="user@example.com",
170
+ login_magic_link_url="https://example.com/authenticate",
171
+ signup_magic_link_url="https://example.com/authenticate",
172
+ login_expiration_minutes=15,
173
+ signup_expiration_minutes=60,
174
+ )
175
+ ```
176
+
177
+ #### Advanced Magic Link with Options
178
+
179
+ ```python
180
+ response = client.magic_links.email.login_or_create(
181
+ email="user@example.com",
182
+ login_magic_link_url="https://example.com/authenticate",
183
+ signup_magic_link_url="https://example.com/authenticate",
184
+ login_expiration_minutes=15,
185
+ signup_expiration_minutes=60,
186
+ login_template_id="custom-login-template",
187
+ signup_template_id="custom-signup-template",
188
+ attributes=stytch.Attributes(
189
+ ip_address="192.168.1.1",
190
+ ),
191
+ code_challenge="challenge_string", # For PKCE flow
192
+ user_id="user-123", # Associate with existing user
193
+ session_duration_minutes=60,
194
+ )
195
+ ```
196
+
197
+ #### Authenticate Magic Link
198
+
199
+ ```python
200
+ # Minimal example
201
+ response = client.magic_links.authenticate(
202
+ token="DOYoip3rvIMMW5lgItikFK-Ak1CfMsgjuiCyI7uuU94=",
203
+ )
204
+
205
+ print(response.user)
206
+ print(response.session_token)
207
+ print(response.session_jwt)
208
+ ```
209
+
210
+ #### Authenticate with Session Options
211
+
212
+ ```python
213
+ response = client.magic_links.authenticate(
214
+ token="DOYoip3rvIMMW5lgItikFK-Ak1CfMsgjuiCyI7uuU94=",
215
+ session_duration_minutes=60,
216
+ session_custom_claims={"custom_claim": "value"},
217
+ attributes=stytch.Attributes(
218
+ ip_address="192.168.1.1",
219
+ user_agent="Mozilla/5.0...",
220
+ ),
221
+ )
222
+ ```
223
+
224
+ #### Send Magic Link Only (No Auto-Create)
225
+
226
+ ```python
227
+ response = client.magic_links.email.send(
228
+ email="user@example.com",
229
+ login_magic_link_url="https://example.com/authenticate",
230
+ login_expiration_minutes=15,
231
+ )
232
+ ```
233
+
234
+ #### Create Embeddable Magic Link
235
+
236
+ ```python
237
+ response = client.magic_links.email.create_embeddable(
238
+ user_id="user-live-123",
239
+ embeddable_magic_link_url="https://example.com/authenticate",
240
+ expiration_minutes=10,
241
+ )
242
+
243
+ print(response.token) # Use in your own emails
244
+ ```
245
+
246
+ #### Async Magic Link Example
247
+
248
+ ```python
249
+ async def send_magic_link(email: str):
250
+ response = await client.magic_links.email.login_or_create_async(
251
+ email=email,
252
+ login_magic_link_url="https://example.com/authenticate",
253
+ )
254
+ return response.user_id
255
+ ```
256
+
257
+ ---
258
+
259
+ ### 2. One-Time Passcodes (OTP)
260
+
261
+ #### SMS OTP
262
+
263
+ **Send OTP via SMS:**
264
+
265
+ ```python
266
+ # Minimal example
267
+ response = client.otps.sms.send(
268
+ phone_number="+15555555555",
269
+ )
270
+
271
+ print(response.phone_id)
272
+ ```
273
+
274
+ **Send OTP with Options:**
275
+
276
+ ```python
277
+ response = client.otps.sms.send(
278
+ phone_number="+15555555555",
279
+ expiration_minutes=10,
280
+ attributes=stytch.Attributes(ip_address="192.168.1.1"),
281
+ user_id="user-123", # Associate with existing user
282
+ )
283
+ ```
284
+
285
+ **Authenticate SMS OTP:**
286
+
287
+ ```python
288
+ # Minimal example
289
+ response = client.otps.authenticate(
290
+ method_id="phone-id-123",
291
+ code="123456",
292
+ )
293
+ ```
294
+
295
+ **Authenticate with Session:**
296
+
297
+ ```python
298
+ response = client.otps.authenticate(
299
+ method_id="phone-id-123",
300
+ code="123456",
301
+ session_duration_minutes=60,
302
+ attributes=stytch.Attributes(ip_address="192.168.1.1"),
303
+ )
304
+
305
+ print(response.user)
306
+ print(response.session_token)
307
+ ```
308
+
309
+ #### Email OTP
310
+
311
+ **Send OTP via Email:**
312
+
313
+ ```python
314
+ # Minimal example
315
+ response = client.otps.email.send(
316
+ email="user@example.com",
317
+ )
318
+ ```
319
+
320
+ **Send with Options:**
321
+
322
+ ```python
323
+ response = client.otps.email.send(
324
+ email="user@example.com",
325
+ expiration_minutes=10,
326
+ login_template_id="custom-template",
327
+ user_id="user-123",
328
+ )
329
+ ```
330
+
331
+ **Authenticate Email OTP:**
332
+
333
+ ```python
334
+ response = client.otps.authenticate(
335
+ method_id="email-id-123",
336
+ code="123456",
337
+ session_duration_minutes=60,
338
+ )
339
+ ```
340
+
341
+ #### WhatsApp OTP
342
+
343
+ **Send OTP via WhatsApp:**
344
+
345
+ ```python
346
+ response = client.otps.whatsapp.send(
347
+ phone_number="+15555555555",
348
+ )
349
+ ```
350
+
351
+ **Authenticate WhatsApp OTP:**
352
+
353
+ ```python
354
+ response = client.otps.authenticate(
355
+ method_id="phone-id-123",
356
+ code="123456",
357
+ )
358
+ ```
359
+
360
+ #### Login or Create User with SMS
361
+
362
+ ```python
363
+ response = client.otps.sms.login_or_create(
364
+ phone_number="+15555555555",
365
+ expiration_minutes=10,
366
+ )
367
+ ```
368
+
369
+ #### Login or Create User with Email
370
+
371
+ ```python
372
+ response = client.otps.email.login_or_create(
373
+ email="user@example.com",
374
+ expiration_minutes=10,
375
+ )
376
+ ```
377
+
378
+ ---
379
+
380
+ ### 3. OAuth
381
+
382
+ OAuth enables authentication via third-party providers.
383
+
384
+ #### Start OAuth Flow
385
+
386
+ ```python
387
+ # Get OAuth authorization URL
388
+ response = client.oauth.start(
389
+ provider="google",
390
+ signup_redirect_url="https://example.com/authenticate",
391
+ login_redirect_url="https://example.com/authenticate",
392
+ )
393
+
394
+ print(response.oauth_url) # Redirect user here
395
+ ```
396
+
397
+ #### Start OAuth with Options
398
+
399
+ ```python
400
+ response = client.oauth.start(
401
+ provider="google",
402
+ signup_redirect_url="https://example.com/authenticate",
403
+ login_redirect_url="https://example.com/authenticate",
404
+ custom_scopes=["https://www.googleapis.com/auth/calendar.readonly"],
405
+ provider_params={"access_type": "offline"},
406
+ code_challenge="challenge_string", # For PKCE
407
+ )
408
+ ```
409
+
410
+ #### Authenticate OAuth
411
+
412
+ ```python
413
+ # Minimal example
414
+ response = client.oauth.authenticate(
415
+ token="oauth-token-from-callback",
416
+ )
417
+
418
+ print(response.user)
419
+ print(response.session_token)
420
+ ```
421
+
422
+ #### Authenticate with Session Options
423
+
424
+ ```python
425
+ response = client.oauth.authenticate(
426
+ token="oauth-token-from-callback",
427
+ session_duration_minutes=60,
428
+ session_custom_claims={"custom_claim": "value"},
429
+ )
430
+ ```
431
+
432
+ #### Supported OAuth Providers
433
+
434
+ - `google`
435
+ - `microsoft`
436
+ - `facebook`
437
+ - `github`
438
+ - `gitlab`
439
+ - `slack`
440
+ - `linkedin`
441
+ - `amazon`
442
+ - `bitbucket`
443
+ - `coinbase`
444
+ - `discord`
445
+ - `figma`
446
+ - `hubspot`
447
+ - `salesforce`
448
+ - `shopify`
449
+ - `snapchat`
450
+ - `tiktok`
451
+ - `twitch`
452
+ - `twitter`
453
+ - `yahoo`
454
+
455
+ ---
456
+
457
+ ### 4. Sessions
458
+
459
+ Sessions manage authenticated user state.
460
+
461
+ #### Authenticate Session
462
+
463
+ ```python
464
+ # Minimal example
465
+ response = client.sessions.authenticate(
466
+ session_token="session-token-here",
467
+ )
468
+
469
+ print(response.user)
470
+ print(response.session) # Contains session data
471
+ ```
472
+
473
+ #### Authenticate with JWT
474
+
475
+ ```python
476
+ response = client.sessions.authenticate_jwt(
477
+ session_jwt="jwt-token-here",
478
+ )
479
+ ```
480
+
481
+ #### Authenticate Session with Refresh
482
+
483
+ ```python
484
+ response = client.sessions.authenticate(
485
+ session_token="session-token-here",
486
+ session_duration_minutes=60, # Extend session
487
+ )
488
+
489
+ # Store new token
490
+ new_session_token = response.session_token
491
+ ```
492
+
493
+ #### Get Sessions
494
+
495
+ ```python
496
+ response = client.sessions.get(
497
+ user_id="user-123",
498
+ )
499
+
500
+ print(response.sessions) # All active sessions
501
+ ```
502
+
503
+ #### Revoke Session
504
+
505
+ ```python
506
+ client.sessions.revoke(
507
+ session_token="session-token-to-revoke",
508
+ )
509
+ ```
510
+
511
+ #### Revoke Session by ID
512
+
513
+ ```python
514
+ client.sessions.revoke(
515
+ session_id="session-id-123",
516
+ )
517
+ ```
518
+
519
+ #### Migrate Session
520
+
521
+ ```python
522
+ response = client.sessions.migrate(
523
+ session_token="old-session-token",
524
+ session_duration_minutes=60,
525
+ )
526
+
527
+ print(response.session_token) # New token
528
+ ```
529
+
530
+ ---
531
+
532
+ ### 5. Users
533
+
534
+ Manage user accounts and data.
535
+
536
+ #### Get User
537
+
538
+ ```python
539
+ response = client.users.get(
540
+ user_id="user-123",
541
+ )
542
+
543
+ print(response.user)
544
+ print(response.user.emails)
545
+ print(response.user.phone_numbers)
546
+ ```
547
+
548
+ #### Create User
549
+
550
+ ```python
551
+ response = client.users.create(
552
+ email="user@example.com",
553
+ name={"first_name": "John", "last_name": "Doe"},
554
+ )
555
+ ```
556
+
557
+ #### Create User with Multiple Methods
558
+
559
+ ```python
560
+ response = client.users.create(
561
+ email="user@example.com",
562
+ phone_number="+15555555555",
563
+ name={"first_name": "John", "last_name": "Doe"},
564
+ attributes={"custom_attribute": "value"},
565
+ )
566
+ ```
567
+
568
+ #### Update User
569
+
570
+ ```python
571
+ response = client.users.update(
572
+ user_id="user-123",
573
+ name={"first_name": "Jane", "last_name": "Smith"},
574
+ attributes={"custom_field": "new_value"},
575
+ )
576
+ ```
577
+
578
+ #### Delete User
579
+
580
+ ```python
581
+ client.users.delete(
582
+ user_id="user-123",
583
+ )
584
+ ```
585
+
586
+ #### Search Users
587
+
588
+ ```python
589
+ # Basic search
590
+ response = client.users.search(
591
+ query={
592
+ "operator": "AND",
593
+ "operands": [
594
+ {
595
+ "filter_name": "status",
596
+ "filter_value": ["active"],
597
+ },
598
+ ],
599
+ },
600
+ )
601
+
602
+ print(response.results)
603
+ ```
604
+
605
+ #### Advanced User Search
606
+
607
+ ```python
608
+ response = client.users.search(
609
+ query={
610
+ "operator": "AND",
611
+ "operands": [
612
+ {
613
+ "filter_name": "email_verified",
614
+ "filter_value": [True],
615
+ },
616
+ {
617
+ "filter_name": "created_at",
618
+ "filter_value": {
619
+ "greater_than": "2024-01-01T00:00:00Z",
620
+ },
621
+ },
622
+ ],
623
+ },
624
+ limit=100,
625
+ cursor="cursor-from-previous-request",
626
+ )
627
+ ```
628
+
629
+ #### Delete Email
630
+
631
+ ```python
632
+ client.users.delete_email(
633
+ email_id="email-id-123",
634
+ )
635
+ ```
636
+
637
+ #### Delete Phone Number
638
+
639
+ ```python
640
+ client.users.delete_phone_number(
641
+ phone_id="phone-id-123",
642
+ )
643
+ ```
644
+
645
+ ---
646
+
647
+ ### 6. Passwords
648
+
649
+ Traditional password authentication.
650
+
651
+ #### Create Password
652
+
653
+ ```python
654
+ response = client.passwords.create(
655
+ email="user@example.com",
656
+ password="SecurePassword123!",
657
+ session_duration_minutes=60,
658
+ )
659
+
660
+ print(response.user)
661
+ print(response.session_token)
662
+ ```
663
+
664
+ #### Authenticate Password
665
+
666
+ ```python
667
+ # Minimal example
668
+ response = client.passwords.authenticate(
669
+ email="user@example.com",
670
+ password="SecurePassword123!",
671
+ )
672
+ ```
673
+
674
+ #### Authenticate with Session
675
+
676
+ ```python
677
+ response = client.passwords.authenticate(
678
+ email="user@example.com",
679
+ password="SecurePassword123!",
680
+ session_duration_minutes=60,
681
+ session_custom_claims={"custom_claim": "value"},
682
+ )
683
+
684
+ print(response.session_token)
685
+ ```
686
+
687
+ #### Strength Check
688
+
689
+ ```python
690
+ response = client.passwords.strength_check(
691
+ email="user@example.com",
692
+ password="password-to-check",
693
+ )
694
+
695
+ print(response.valid_password)
696
+ print(response.score)
697
+ print(response.breached_password)
698
+ print(response.strength_policy)
699
+ print(response.breach_detection_on_create)
700
+ ```
701
+
702
+ #### Initiate Password Reset
703
+
704
+ ```python
705
+ response = client.passwords.email.reset_start(
706
+ email="user@example.com",
707
+ reset_password_redirect_url="https://example.com/reset",
708
+ reset_password_expiration_minutes=30,
709
+ )
710
+ ```
711
+
712
+ #### Reset Password by Email
713
+
714
+ ```python
715
+ response = client.passwords.email.reset(
716
+ token="reset-token-from-email",
717
+ password="NewSecurePassword123!",
718
+ session_duration_minutes=60,
719
+ )
720
+
721
+ print(response.user)
722
+ print(response.session_token)
723
+ ```
724
+
725
+ #### Reset Password by Session
726
+
727
+ ```python
728
+ response = client.passwords.session.reset(
729
+ session_token="active-session-token",
730
+ password="NewSecurePassword123!",
731
+ )
732
+ ```
733
+
734
+ #### Migrate Password (from existing system)
735
+
736
+ ```python
737
+ response = client.passwords.migrate(
738
+ email="user@example.com",
739
+ hash="$2a$10$...", # bcrypt hash
740
+ hash_type="bcrypt",
741
+ name={"first_name": "John", "last_name": "Doe"},
742
+ )
743
+ ```
744
+
745
+ ---
746
+
747
+ ### 7. WebAuthn
748
+
749
+ Passwordless authentication using biometrics or security keys.
750
+
751
+ #### Register WebAuthn Start
752
+
753
+ ```python
754
+ response = client.webauthn.register_start(
755
+ user_id="user-123",
756
+ domain="example.com",
757
+ authenticator_type="platform", # or "cross-platform"
758
+ )
759
+
760
+ print(response.public_key_credential_creation_options)
761
+ ```
762
+
763
+ #### Register WebAuthn Finish
764
+
765
+ ```python
766
+ response = client.webauthn.register(
767
+ user_id="user-123",
768
+ public_key_credential="credential-from-client",
769
+ session_duration_minutes=60,
770
+ )
771
+
772
+ print(response.webauthn_registration_id)
773
+ print(response.session_token)
774
+ ```
775
+
776
+ #### Authenticate WebAuthn Start
777
+
778
+ ```python
779
+ response = client.webauthn.authenticate_start(
780
+ domain="example.com",
781
+ user_id="user-123", # Optional
782
+ )
783
+
784
+ print(response.public_key_credential_request_options)
785
+ ```
786
+
787
+ #### Authenticate WebAuthn Finish
788
+
789
+ ```python
790
+ response = client.webauthn.authenticate(
791
+ public_key_credential="credential-from-client",
792
+ session_duration_minutes=60,
793
+ )
794
+
795
+ print(response.user)
796
+ print(response.session_token)
797
+ ```
798
+
799
+ #### Update WebAuthn
800
+
801
+ ```python
802
+ response = client.webauthn.update(
803
+ webauthn_registration_id="webauthn-reg-id-123",
804
+ name="My Fingerprint",
805
+ )
806
+ ```
807
+
808
+ ---
809
+
810
+ ### 8. TOTP (Time-based One-Time Passwords)
811
+
812
+ #### Create TOTP
813
+
814
+ ```python
815
+ response = client.totps.create(
816
+ user_id="user-123",
817
+ expiration_minutes=10,
818
+ )
819
+
820
+ print(response.secret)
821
+ print(response.qr_code) # URL for QR code image
822
+ print(response.recovery_codes)
823
+ ```
824
+
825
+ #### Authenticate TOTP
826
+
827
+ ```python
828
+ response = client.totps.authenticate(
829
+ user_id="user-123",
830
+ totp_code="123456",
831
+ session_duration_minutes=60,
832
+ )
833
+
834
+ print(response.session_token)
835
+ ```
836
+
837
+ #### Get TOTPs
838
+
839
+ ```python
840
+ response = client.totps.get(
841
+ user_id="user-123",
842
+ )
843
+
844
+ print(response.totps)
845
+ ```
846
+
847
+ #### Recover TOTP
848
+
849
+ ```python
850
+ response = client.totps.recovery_codes(
851
+ user_id="user-123",
852
+ recovery_code="recovery-code-string",
853
+ session_duration_minutes=60,
854
+ )
855
+
856
+ print(response.session_token)
857
+ ```
858
+
859
+ ---
860
+
861
+ ### 9. Crypto Wallets (Web3)
862
+
863
+ #### Authenticate Wallet Start
864
+
865
+ ```python
866
+ response = client.crypto_wallets.authenticate_start(
867
+ crypto_wallet_address="0x1234...",
868
+ crypto_wallet_type="ethereum",
869
+ )
870
+
871
+ print(response.challenge)
872
+ ```
873
+
874
+ #### Authenticate Wallet Finish
875
+
876
+ ```python
877
+ response = client.crypto_wallets.authenticate(
878
+ crypto_wallet_address="0x1234...",
879
+ crypto_wallet_type="ethereum",
880
+ signature="signed-challenge",
881
+ session_duration_minutes=60,
882
+ )
883
+
884
+ print(response.user)
885
+ print(response.session_token)
886
+ ```
887
+
888
+ ---
889
+
890
+ ### 10. M2M (Machine-to-Machine)
891
+
892
+ #### Authenticate M2M Token
893
+
894
+ ```python
895
+ response = client.m2m.authenticate_token(
896
+ access_token="m2m-access-token",
897
+ )
898
+
899
+ print(response.member_id)
900
+ print(response.scopes)
901
+ ```
902
+
903
+ #### Get M2M Client
904
+
905
+ ```python
906
+ response = client.m2m.clients.get(
907
+ client_id="client-id-123",
908
+ )
909
+
910
+ print(response.m2m_client)
911
+ ```
912
+
913
+ ---
914
+
915
+ ## B2B API Surfaces
916
+
917
+ ### 1. Organizations
918
+
919
+ #### Create Organization
920
+
921
+ ```python
922
+ response = b2b_client.organizations.create(
923
+ organization_name="Acme Corp",
924
+ organization_slug="acme",
925
+ email_allowed_domains=["acme.com"],
926
+ )
927
+
928
+ print(response.organization)
929
+ ```
930
+
931
+ #### Get Organization
932
+
933
+ ```python
934
+ response = b2b_client.organizations.get(
935
+ organization_id="org-123",
936
+ )
937
+
938
+ print(response.organization)
939
+ ```
940
+
941
+ #### Update Organization
942
+
943
+ ```python
944
+ response = b2b_client.organizations.update(
945
+ organization_id="org-123",
946
+ organization_name="Acme Corporation",
947
+ )
948
+ ```
949
+
950
+ #### Delete Organization
951
+
952
+ ```python
953
+ b2b_client.organizations.delete(
954
+ organization_id="org-123",
955
+ )
956
+ ```
957
+
958
+ ---
959
+
960
+ ### 2. Members
961
+
962
+ #### Create Member
963
+
964
+ ```python
965
+ response = b2b_client.organizations.members.create(
966
+ organization_id="org-123",
967
+ email_address="member@acme.com",
968
+ name="John Doe",
969
+ is_breakglass=False,
970
+ )
971
+
972
+ print(response.member)
973
+ ```
974
+
975
+ #### Get Member
976
+
977
+ ```python
978
+ response = b2b_client.organizations.members.get(
979
+ organization_id="org-123",
980
+ member_id="member-123",
981
+ )
982
+
983
+ print(response.member)
984
+ ```
985
+
986
+ #### Update Member
987
+
988
+ ```python
989
+ response = b2b_client.organizations.members.update(
990
+ organization_id="org-123",
991
+ member_id="member-123",
992
+ name="Jane Doe",
993
+ )
994
+ ```
995
+
996
+ #### Delete Member
997
+
998
+ ```python
999
+ b2b_client.organizations.members.delete(
1000
+ organization_id="org-123",
1001
+ member_id="member-123",
1002
+ )
1003
+ ```
1004
+
1005
+ ---
1006
+
1007
+ ### 3. B2B Magic Links
1008
+
1009
+ #### Send B2B Magic Link
1010
+
1011
+ ```python
1012
+ response = b2b_client.magic_links.email.login_or_signup(
1013
+ organization_id="org-123",
1014
+ email_address="member@acme.com",
1015
+ login_redirect_url="https://acme.com/authenticate",
1016
+ signup_redirect_url="https://acme.com/authenticate",
1017
+ )
1018
+ ```
1019
+
1020
+ #### Authenticate B2B Magic Link
1021
+
1022
+ ```python
1023
+ response = b2b_client.magic_links.authenticate(
1024
+ magic_links_token="token-from-email",
1025
+ session_duration_minutes=60,
1026
+ )
1027
+
1028
+ print(response.member)
1029
+ print(response.organization)
1030
+ print(response.session_token)
1031
+ ```
1032
+
1033
+ ---
1034
+
1035
+ ### 4. B2B Sessions
1036
+
1037
+ #### Authenticate B2B Session
1038
+
1039
+ ```python
1040
+ response = b2b_client.sessions.authenticate(
1041
+ session_token="session-token-here",
1042
+ )
1043
+
1044
+ print(response.member)
1045
+ print(response.organization)
1046
+ print(response.session)
1047
+ ```
1048
+
1049
+ #### Authenticate B2B JWT
1050
+
1051
+ ```python
1052
+ response = b2b_client.sessions.authenticate_jwt(
1053
+ session_jwt="jwt-token-here",
1054
+ )
1055
+ ```
1056
+
1057
+ #### Get B2B Sessions
1058
+
1059
+ ```python
1060
+ response = b2b_client.sessions.get(
1061
+ organization_id="org-123",
1062
+ member_id="member-123",
1063
+ )
1064
+
1065
+ print(response.sessions)
1066
+ ```
1067
+
1068
+ #### Revoke B2B Session
1069
+
1070
+ ```python
1071
+ b2b_client.sessions.revoke(
1072
+ session_token="session-token-to-revoke",
1073
+ )
1074
+ ```
1075
+
1076
+ ---
1077
+
1078
+ ### 5. B2B OAuth
1079
+
1080
+ #### Start B2B OAuth
1081
+
1082
+ ```python
1083
+ response = b2b_client.oauth.start(
1084
+ organization_id="org-123",
1085
+ provider="google",
1086
+ login_redirect_url="https://acme.com/authenticate",
1087
+ signup_redirect_url="https://acme.com/authenticate",
1088
+ )
1089
+
1090
+ print(response.oauth_url)
1091
+ ```
1092
+
1093
+ #### Authenticate B2B OAuth
1094
+
1095
+ ```python
1096
+ response = b2b_client.oauth.authenticate(
1097
+ oauth_token="token-from-callback",
1098
+ session_duration_minutes=60,
1099
+ )
1100
+
1101
+ print(response.member)
1102
+ print(response.organization)
1103
+ print(response.session_token)
1104
+ ```
1105
+
1106
+ ---
1107
+
1108
+ ### 6. B2B SMS OTP
1109
+
1110
+ #### Send B2B SMS OTP
1111
+
1112
+ ```python
1113
+ response = b2b_client.otps.sms.send(
1114
+ organization_id="org-123",
1115
+ member_id="member-123",
1116
+ phone_number="+15555555555",
1117
+ )
1118
+ ```
1119
+
1120
+ #### Authenticate B2B SMS OTP
1121
+
1122
+ ```python
1123
+ response = b2b_client.otps.sms.authenticate(
1124
+ organization_id="org-123",
1125
+ member_id="member-123",
1126
+ code="123456",
1127
+ session_duration_minutes=60,
1128
+ )
1129
+
1130
+ print(response.member)
1131
+ print(response.session_token)
1132
+ ```
1133
+
1134
+ ---
1135
+
1136
+ ### 7. SSO (Single Sign-On)
1137
+
1138
+ #### Start SSO
1139
+
1140
+ ```python
1141
+ response = b2b_client.sso.start(
1142
+ connection_id="sso-connection-123",
1143
+ login_redirect_url="https://acme.com/authenticate",
1144
+ signup_redirect_url="https://acme.com/authenticate",
1145
+ )
1146
+
1147
+ print(response.sso_url)
1148
+ ```
1149
+
1150
+ #### Authenticate SSO
1151
+
1152
+ ```python
1153
+ response = b2b_client.sso.authenticate(
1154
+ sso_token="token-from-sso-provider",
1155
+ session_duration_minutes=60,
1156
+ )
1157
+
1158
+ print(response.member)
1159
+ print(response.organization)
1160
+ ```
1161
+
1162
+ #### Get SSO Connections
1163
+
1164
+ ```python
1165
+ response = b2b_client.sso.get_connections(
1166
+ organization_id="org-123",
1167
+ )
1168
+
1169
+ print(response.saml_connections)
1170
+ print(response.oidc_connections)
1171
+ ```
1172
+
1173
+ #### Create SAML Connection
1174
+
1175
+ ```python
1176
+ response = b2b_client.sso.saml.create_connection(
1177
+ organization_id="org-123",
1178
+ display_name="Acme SAML",
1179
+ )
1180
+
1181
+ print(response.connection)
1182
+ ```
1183
+
1184
+ #### Update SAML Connection
1185
+
1186
+ ```python
1187
+ response = b2b_client.sso.saml.update_connection(
1188
+ organization_id="org-123",
1189
+ connection_id="saml-connection-123",
1190
+ idp_entity_id="https://idp.acme.com/entity",
1191
+ idp_sso_url="https://idp.acme.com/sso",
1192
+ attribute_mapping={
1193
+ "email": "email",
1194
+ "first_name": "firstName",
1195
+ "last_name": "lastName",
1196
+ },
1197
+ x509_certificate="certificate-string",
1198
+ )
1199
+ ```
1200
+
1201
+ #### Create OIDC Connection
1202
+
1203
+ ```python
1204
+ response = b2b_client.sso.oidc.create_connection(
1205
+ organization_id="org-123",
1206
+ display_name="Acme OIDC",
1207
+ )
1208
+
1209
+ print(response.connection)
1210
+ ```
1211
+
1212
+ #### Update OIDC Connection
1213
+
1214
+ ```python
1215
+ response = b2b_client.sso.oidc.update_connection(
1216
+ organization_id="org-123",
1217
+ connection_id="oidc-connection-123",
1218
+ issuer="https://idp.acme.com",
1219
+ client_id="client-id",
1220
+ client_secret="client-secret",
1221
+ authorization_url="https://idp.acme.com/authorize",
1222
+ token_url="https://idp.acme.com/token",
1223
+ userinfo_url="https://idp.acme.com/userinfo",
1224
+ )
1225
+ ```
1226
+
1227
+ ---
1228
+
1229
+ ### 8. Discovery
1230
+
1231
+ Discovery allows users to find and join organizations.
1232
+
1233
+ #### Create Organization via Discovery
1234
+
1235
+ ```python
1236
+ response = b2b_client.discovery.organizations.create(
1237
+ intermediate_session_token="token-from-discovery",
1238
+ organization_name="New Org",
1239
+ organization_slug="new-org",
1240
+ session_duration_minutes=60,
1241
+ )
1242
+
1243
+ print(response.organization)
1244
+ print(response.session_token)
1245
+ ```
1246
+
1247
+ #### List Discovered Organizations
1248
+
1249
+ ```python
1250
+ response = b2b_client.discovery.organizations.list(
1251
+ intermediate_session_token="token-from-discovery",
1252
+ )
1253
+
1254
+ print(response.discovered_organizations)
1255
+ ```
1256
+
1257
+ ---
1258
+
1259
+ ### 9. RBAC (Role-Based Access Control)
1260
+
1261
+ #### Check Member Permissions
1262
+
1263
+ ```python
1264
+ response = b2b_client.rbac.policy(
1265
+ organization_id="org-123",
1266
+ member_id="member-123",
1267
+ resource_id="document-123",
1268
+ action="read",
1269
+ )
1270
+
1271
+ print(response.authorized)
1272
+ ```
1273
+
1274
+ ---
1275
+
1276
+ ## Error Handling
1277
+
1278
+ ### Basic Error Handling
1279
+
1280
+ ```python
1281
+ from stytch.core.response_base import StytchError
1282
+
1283
+ try:
1284
+ response = client.magic_links.email.login_or_create(
1285
+ email="user@example.com",
1286
+ )
1287
+ print(response.user_id)
1288
+ except StytchError as error:
1289
+ print("Error:", error.details.error_type)
1290
+ print("Message:", error.details.error_message)
1291
+ print("URL:", error.details.error_url)
1292
+ ```
1293
+
1294
+ ### Detailed Error Handling
1295
+
1296
+ ```python
1297
+ from stytch.core.response_base import StytchError
1298
+
1299
+ try:
1300
+ response = client.magic_links.authenticate(token=token)
1301
+ except StytchError as error:
1302
+ if error.details.error_type == "unable_to_auth_magic_link":
1303
+ # Token invalid, expired, or already used
1304
+ print("Invalid or expired magic link")
1305
+ elif error.details.error_type == "invalid_token":
1306
+ # Malformed token
1307
+ print("Invalid token format")
1308
+ else:
1309
+ # Other errors
1310
+ print("Authentication failed")
1311
+ ```
1312
+
1313
+ ### Common Error Types
1314
+
1315
+ - `unable_to_auth_magic_link` - Magic link token invalid, expired, or used
1316
+ - `invalid_token` - Token format is invalid
1317
+ - `session_not_found` - Session doesn't exist
1318
+ - `user_not_found` - User doesn't exist
1319
+ - `duplicate_email` - Email already exists
1320
+ - `invalid_credentials` - Password authentication failed
1321
+ - `rate_limit_exceeded` - Too many requests
1322
+ - `unauthorized` - API credentials invalid
1323
+
1324
+ ---
1325
+
1326
+ ## Flask Integration Example
1327
+
1328
+ ### Complete Auth Flow
1329
+
1330
+ ```python
1331
+ from flask import Flask, request, session, redirect, jsonify
1332
+ from stytch import Client
1333
+ from stytch.core.response_base import StytchError
1334
+ import os
1335
+
1336
+ app = Flask(__name__)
1337
+ app.secret_key = os.getenv("FLASK_SECRET_KEY")
1338
+
1339
+ stytch_client = Client(
1340
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
1341
+ secret=os.getenv("STYTCH_SECRET"),
1342
+ )
1343
+
1344
+ @app.route("/login", methods=["POST"])
1345
+ def login():
1346
+ try:
1347
+ email = request.json.get("email")
1348
+ response = stytch_client.magic_links.email.login_or_create(
1349
+ email=email,
1350
+ login_magic_link_url=f"{os.getenv('BASE_URL')}/authenticate",
1351
+ signup_magic_link_url=f"{os.getenv('BASE_URL')}/authenticate",
1352
+ )
1353
+ return jsonify({"success": True, "user_id": response.user_id})
1354
+ except StytchError as error:
1355
+ return jsonify({"error": error.details.error_message}), 400
1356
+
1357
+ @app.route("/authenticate")
1358
+ def authenticate():
1359
+ try:
1360
+ token = request.args.get("token")
1361
+ response = stytch_client.magic_links.authenticate(
1362
+ token=token,
1363
+ session_duration_minutes=60,
1364
+ )
1365
+
1366
+ session["stytch_session_token"] = response.session_token
1367
+ session["user_id"] = response.user.user_id
1368
+
1369
+ return redirect("/dashboard")
1370
+ except StytchError:
1371
+ return "Authentication failed", 400
1372
+
1373
+ def authenticate_session():
1374
+ """Middleware to authenticate session"""
1375
+ session_token = session.get("stytch_session_token")
1376
+
1377
+ if not session_token:
1378
+ return None
1379
+
1380
+ try:
1381
+ response = stytch_client.sessions.authenticate(
1382
+ session_token=session_token,
1383
+ )
1384
+ session["stytch_session_token"] = response.session_token
1385
+ return response.user
1386
+ except StytchError:
1387
+ session.clear()
1388
+ return None
1389
+
1390
+ @app.route("/dashboard")
1391
+ def dashboard():
1392
+ user = authenticate_session()
1393
+ if not user:
1394
+ return jsonify({"error": "Not authenticated"}), 401
1395
+ return jsonify({"user": user})
1396
+
1397
+ @app.route("/logout", methods=["POST"])
1398
+ def logout():
1399
+ try:
1400
+ stytch_client.sessions.revoke(
1401
+ session_token=session.get("stytch_session_token"),
1402
+ )
1403
+ session.clear()
1404
+ return jsonify({"success": True})
1405
+ except StytchError as error:
1406
+ return jsonify({"error": error.details.error_message}), 400
1407
+
1408
+ if __name__ == "__main__":
1409
+ app.run(port=3000)
1410
+ ```
1411
+
1412
+ ---
1413
+
1414
+ ## FastAPI Integration Example
1415
+
1416
+ ### Complete Auth Flow with FastAPI
1417
+
1418
+ ```python
1419
+ from fastapi import FastAPI, HTTPException, Depends, Request, Response
1420
+ from fastapi.responses import RedirectResponse
1421
+ from pydantic import BaseModel
1422
+ import stytch
1423
+ import os
1424
+
1425
+ app = FastAPI()
1426
+
1427
+ client = stytch.Client(
1428
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
1429
+ secret=os.getenv("STYTCH_SECRET"),
1430
+ )
1431
+
1432
+ class LoginRequest(BaseModel):
1433
+ email: str
1434
+
1435
+ @app.post("/login")
1436
+ async def login(data: LoginRequest):
1437
+ try:
1438
+ response = client.magic_links.email.login_or_create(
1439
+ email=data.email,
1440
+ login_magic_link_url=f"{os.getenv('BASE_URL')}/authenticate",
1441
+ )
1442
+ return {"success": True, "user_id": response.user_id}
1443
+ except stytch.core.response_base.StytchError as error:
1444
+ raise HTTPException(
1445
+ status_code=400,
1446
+ detail=error.details.error_message
1447
+ )
1448
+
1449
+ @app.get("/authenticate")
1450
+ async def authenticate(token: str, response: Response):
1451
+ try:
1452
+ auth_response = client.magic_links.authenticate(
1453
+ token=token,
1454
+ session_duration_minutes=60,
1455
+ )
1456
+
1457
+ response.set_cookie(
1458
+ key="stytch_session",
1459
+ value=auth_response.session_token,
1460
+ httponly=True,
1461
+ secure=True,
1462
+ max_age=3600,
1463
+ )
1464
+
1465
+ return RedirectResponse(url="/dashboard")
1466
+ except stytch.core.response_base.StytchError:
1467
+ raise HTTPException(status_code=400, detail="Authentication failed")
1468
+
1469
+ async def get_current_user(request: Request):
1470
+ session_token = request.cookies.get("stytch_session")
1471
+
1472
+ if not session_token:
1473
+ raise HTTPException(status_code=401, detail="Not authenticated")
1474
+
1475
+ try:
1476
+ response = client.sessions.authenticate(
1477
+ session_token=session_token,
1478
+ )
1479
+ return response.user
1480
+ except stytch.core.response_base.StytchError:
1481
+ raise HTTPException(status_code=401, detail="Invalid session")
1482
+
1483
+ @app.get("/dashboard")
1484
+ async def dashboard(user=Depends(get_current_user)):
1485
+ return {"user": user}
1486
+
1487
+ @app.post("/logout")
1488
+ async def logout(request: Request, response: Response):
1489
+ session_token = request.cookies.get("stytch_session")
1490
+
1491
+ if session_token:
1492
+ try:
1493
+ client.sessions.revoke(session_token=session_token)
1494
+ except stytch.core.response_base.StytchError:
1495
+ pass
1496
+
1497
+ response.delete_cookie("stytch_session")
1498
+ return {"success": True}
1499
+ ```
1500
+
1501
+ ---
1502
+
1503
+ ## Django Integration Example
1504
+
1505
+ ### Django Middleware for Stytch
1506
+
1507
+ ```python
1508
+ # middleware.py
1509
+ import stytch
1510
+ import os
1511
+ from django.utils.functional import SimpleLazyObject
1512
+
1513
+ client = stytch.Client(
1514
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
1515
+ secret=os.getenv("STYTCH_SECRET"),
1516
+ )
1517
+
1518
+ def get_user(request):
1519
+ session_token = request.session.get("stytch_session_token")
1520
+
1521
+ if not session_token:
1522
+ return None
1523
+
1524
+ try:
1525
+ response = client.sessions.authenticate(
1526
+ session_token=session_token,
1527
+ )
1528
+ request.session["stytch_session_token"] = response.session_token
1529
+ return response.user
1530
+ except stytch.core.response_base.StytchError:
1531
+ return None
1532
+
1533
+ class StytchAuthenticationMiddleware:
1534
+ def __init__(self, get_response):
1535
+ self.get_response = get_response
1536
+
1537
+ def __call__(self, request):
1538
+ request.stytch_user = SimpleLazyObject(lambda: get_user(request))
1539
+ response = self.get_response(request)
1540
+ return response
1541
+ ```
1542
+
1543
+ ### Django Views
1544
+
1545
+ ```python
1546
+ # views.py
1547
+ from django.http import JsonResponse, HttpResponseRedirect
1548
+ from django.views.decorators.http import require_http_methods
1549
+ from stytch.core.response_base import StytchError
1550
+ import json
1551
+
1552
+ @require_http_methods(["POST"])
1553
+ def login(request):
1554
+ try:
1555
+ data = json.loads(request.body)
1556
+ email = data.get("email")
1557
+
1558
+ response = client.magic_links.email.login_or_create(
1559
+ email=email,
1560
+ login_magic_link_url=f"{os.getenv('BASE_URL')}/authenticate",
1561
+ )
1562
+
1563
+ return JsonResponse({
1564
+ "success": True,
1565
+ "user_id": response.user_id
1566
+ })
1567
+ except StytchError as error:
1568
+ return JsonResponse({
1569
+ "error": error.details.error_message
1570
+ }, status=400)
1571
+
1572
+ def authenticate(request):
1573
+ try:
1574
+ token = request.GET.get("token")
1575
+ response = client.magic_links.authenticate(
1576
+ token=token,
1577
+ session_duration_minutes=60,
1578
+ )
1579
+
1580
+ request.session["stytch_session_token"] = response.session_token
1581
+ request.session["user_id"] = response.user.user_id
1582
+
1583
+ return HttpResponseRedirect("/dashboard")
1584
+ except StytchError:
1585
+ return JsonResponse({"error": "Authentication failed"}, status=400)
1586
+
1587
+ @require_http_methods(["POST"])
1588
+ def logout(request):
1589
+ try:
1590
+ session_token = request.session.get("stytch_session_token")
1591
+ if session_token:
1592
+ client.sessions.revoke(session_token=session_token)
1593
+ request.session.flush()
1594
+ return JsonResponse({"success": True})
1595
+ except StytchError as error:
1596
+ return JsonResponse({
1597
+ "error": error.details.error_message
1598
+ }, status=400)
1599
+ ```
1600
+
1601
+ ---
1602
+
1603
+ ## Async/Await Support
1604
+
1605
+ All SDK methods support async by appending `_async`:
1606
+
1607
+ ### Async Example with asyncio
1608
+
1609
+ ```python
1610
+ import asyncio
1611
+ import stytch
1612
+ import os
1613
+
1614
+ async def main():
1615
+ client = stytch.Client(
1616
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
1617
+ secret=os.getenv("STYTCH_SECRET"),
1618
+ )
1619
+
1620
+ # Send magic link
1621
+ response = await client.magic_links.email.login_or_create_async(
1622
+ email="user@example.com",
1623
+ login_magic_link_url="https://example.com/authenticate",
1624
+ )
1625
+ print(f"Magic link sent to user: {response.user_id}")
1626
+
1627
+ # Authenticate (in a real app, this would happen after user clicks link)
1628
+ auth_response = await client.magic_links.authenticate_async(
1629
+ token="token-from-email",
1630
+ )
1631
+ print(f"User authenticated: {auth_response.user.user_id}")
1632
+
1633
+ # Validate session
1634
+ session_response = await client.sessions.authenticate_async(
1635
+ session_token=auth_response.session_token,
1636
+ )
1637
+ print(f"Session valid for: {session_response.user.emails}")
1638
+
1639
+ asyncio.run(main())
1640
+ ```
1641
+
1642
+ ### Async with FastAPI
1643
+
1644
+ ```python
1645
+ from fastapi import FastAPI
1646
+ import stytch
1647
+ import os
1648
+
1649
+ app = FastAPI()
1650
+
1651
+ client = stytch.Client(
1652
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
1653
+ secret=os.getenv("STYTCH_SECRET"),
1654
+ )
1655
+
1656
+ @app.post("/login")
1657
+ async def login(email: str):
1658
+ response = await client.magic_links.email.login_or_create_async(
1659
+ email=email,
1660
+ login_magic_link_url="https://example.com/authenticate",
1661
+ )
1662
+ return {"user_id": response.user_id}
1663
+
1664
+ @app.get("/authenticate")
1665
+ async def authenticate(token: str):
1666
+ response = await client.magic_links.authenticate_async(
1667
+ token=token,
1668
+ session_duration_minutes=60,
1669
+ )
1670
+ return {"session_token": response.session_token}
1671
+ ```
1672
+
1673
+ ---
1674
+
1675
+ ## Testing
1676
+
1677
+ ### Using Test Environment
1678
+
1679
+ ```python
1680
+ client = stytch.Client(
1681
+ project_id=os.getenv("STYTCH_TEST_PROJECT_ID"),
1682
+ secret=os.getenv("STYTCH_TEST_SECRET"),
1683
+ environment="test",
1684
+ )
1685
+ ```
1686
+
1687
+ ### Test Mode Magic Links
1688
+
1689
+ In test mode, use these special test emails:
1690
+ - `sandbox@stytch.com` - Always succeeds
1691
+
1692
+ ### Mock Testing with pytest
1693
+
1694
+ ```python
1695
+ import pytest
1696
+ from unittest.mock import Mock, patch
1697
+ import stytch
1698
+
1699
+ @pytest.fixture
1700
+ def mock_stytch_client():
1701
+ with patch('stytch.Client') as mock:
1702
+ client = mock.return_value
1703
+
1704
+ # Mock magic links
1705
+ client.magic_links.email.login_or_create = Mock(
1706
+ return_value=Mock(
1707
+ user_id="user-test-123",
1708
+ email_id="email-test-123",
1709
+ )
1710
+ )
1711
+
1712
+ client.magic_links.authenticate = Mock(
1713
+ return_value=Mock(
1714
+ user=Mock(
1715
+ user_id="user-test-123",
1716
+ emails=[{"email": "test@example.com"}],
1717
+ ),
1718
+ session_token="test-session-token",
1719
+ )
1720
+ )
1721
+
1722
+ yield client
1723
+
1724
+ def test_login(mock_stytch_client):
1725
+ response = mock_stytch_client.magic_links.email.login_or_create(
1726
+ email="test@example.com"
1727
+ )
1728
+ assert response.user_id == "user-test-123"
1729
+ ```
1730
+
1731
+ ---
1732
+
1733
+ ## Webhooks
1734
+
1735
+ ### Verify Webhook Signature
1736
+
1737
+ ```python
1738
+ import hmac
1739
+ import hashlib
1740
+
1741
+ def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
1742
+ expected_signature = hmac.new(
1743
+ secret.encode(),
1744
+ payload,
1745
+ hashlib.sha256
1746
+ ).hexdigest()
1747
+
1748
+ return signature == expected_signature
1749
+
1750
+ # Flask webhook endpoint
1751
+ @app.route("/webhooks/stytch", methods=["POST"])
1752
+ def stytch_webhook():
1753
+ signature = request.headers.get("stytch-signature")
1754
+ payload = request.get_data()
1755
+
1756
+ is_valid = verify_webhook_signature(
1757
+ payload,
1758
+ signature,
1759
+ os.getenv("STYTCH_WEBHOOK_SECRET")
1760
+ )
1761
+
1762
+ if not is_valid:
1763
+ return "Invalid signature", 401
1764
+
1765
+ event = request.json
1766
+ print(f"Webhook event: {event['event_type']}")
1767
+
1768
+ return jsonify({"received": True})
1769
+ ```
1770
+
1771
+ ### FastAPI Webhook Handler
1772
+
1773
+ ```python
1774
+ from fastapi import FastAPI, Request, HTTPException
1775
+ import hmac
1776
+ import hashlib
1777
+
1778
+ @app.post("/webhooks/stytch")
1779
+ async def stytch_webhook(request: Request):
1780
+ signature = request.headers.get("stytch-signature")
1781
+ payload = await request.body()
1782
+
1783
+ expected_signature = hmac.new(
1784
+ os.getenv("STYTCH_WEBHOOK_SECRET").encode(),
1785
+ payload,
1786
+ hashlib.sha256
1787
+ ).hexdigest()
1788
+
1789
+ if signature != expected_signature:
1790
+ raise HTTPException(status_code=401, detail="Invalid signature")
1791
+
1792
+ event = await request.json()
1793
+ print(f"Webhook event: {event['event_type']}")
1794
+
1795
+ return {"received": True}
1796
+ ```
1797
+
1798
+ ### Webhook Event Types
1799
+
1800
+ - `user.created` - New user created
1801
+ - `user.updated` - User data updated
1802
+ - `user.deleted` - User deleted
1803
+ - `session.created` - New session created
1804
+ - `session.revoked` - Session revoked
1805
+ - `magic_link.sent` - Magic link sent
1806
+ - `otp.sent` - OTP sent
1807
+ - `password.strength_check_failed` - Weak password detected
1808
+
1809
+ ---
1810
+
1811
+ ## Advanced Use Cases
1812
+
1813
+ ### Multi-Factor Authentication Flow
1814
+
1815
+ ```python
1816
+ # Step 1: Primary authentication (password)
1817
+ @app.post("/login")
1818
+ def login():
1819
+ email = request.json.get("email")
1820
+ password = request.json.get("password")
1821
+
1822
+ try:
1823
+ response = client.passwords.authenticate(
1824
+ email=email,
1825
+ password=password,
1826
+ session_duration_minutes=5, # Short session for MFA
1827
+ )
1828
+
1829
+ return jsonify({
1830
+ "requires_mfa": True,
1831
+ "intermediate_session_token": response.session_token,
1832
+ "user_id": response.user_id,
1833
+ })
1834
+ except StytchError:
1835
+ return jsonify({"error": "Invalid credentials"}), 401
1836
+
1837
+ # Step 2: MFA with TOTP
1838
+ @app.post("/verify-totp")
1839
+ def verify_totp():
1840
+ user_id = request.json.get("user_id")
1841
+ totp_code = request.json.get("totp_code")
1842
+
1843
+ try:
1844
+ response = client.totps.authenticate(
1845
+ user_id=user_id,
1846
+ totp_code=totp_code,
1847
+ session_duration_minutes=60, # Full session after MFA
1848
+ )
1849
+
1850
+ return jsonify({
1851
+ "session_token": response.session_token,
1852
+ "user": response.user,
1853
+ })
1854
+ except StytchError:
1855
+ return jsonify({"error": "Invalid TOTP code"}), 401
1856
+ ```
1857
+
1858
+ ### Custom Email Templates
1859
+
1860
+ ```python
1861
+ response = client.magic_links.email.login_or_create(
1862
+ email="user@example.com",
1863
+ login_magic_link_url="https://example.com/authenticate",
1864
+ login_template_id="custom-login-template-id",
1865
+ signup_template_id="custom-signup-template-id",
1866
+ )
1867
+ ```
1868
+
1869
+ ### IP and User Agent Matching
1870
+
1871
+ ```python
1872
+ response = client.magic_links.authenticate(
1873
+ token=token,
1874
+ attributes=stytch.Attributes(
1875
+ ip_address=request.remote_addr,
1876
+ user_agent=request.headers.get("User-Agent"),
1877
+ ),
1878
+ )
1879
+ ```
1880
+
1881
+ ---
1882
+
1883
+ ## Rate Limiting
1884
+
1885
+ Stytch implements rate limiting on authentication endpoints. Handle rate limit errors:
1886
+
1887
+ ```python
1888
+ from stytch.core.response_base import StytchError
1889
+
1890
+ try:
1891
+ client.otps.sms.send(phone_number=phone_number)
1892
+ except StytchError as error:
1893
+ if error.details.error_type == "rate_limit_exceeded":
1894
+ retry_after = error.details.retry_after # seconds
1895
+ return jsonify({
1896
+ "error": "Too many requests",
1897
+ "retry_after": retry_after,
1898
+ }), 429
1899
+ ```
1900
+
1901
+ ---
1902
+
1903
+ ## Migration from Other Auth Systems
1904
+
1905
+ ### Import Users with Passwords
1906
+
1907
+ ```python
1908
+ # Migrate user from bcrypt-based system
1909
+ client.passwords.migrate(
1910
+ email="user@example.com",
1911
+ hash="$2a$10$existing_bcrypt_hash",
1912
+ hash_type="bcrypt",
1913
+ name={"first_name": "John", "last_name": "Doe"},
1914
+ )
1915
+ ```
1916
+
1917
+ ### Supported Hash Types
1918
+
1919
+ - `bcrypt`
1920
+ - `md_5`
1921
+ - `argon_2i`
1922
+ - `argon_2id`
1923
+ - `sha_1`
1924
+ - `scrypt`
1925
+ - `phpass`
1926
+ - `pbkdf_2`
1927
+
1928
+ ---
1929
+
1930
+ ## Type Hints
1931
+
1932
+ The SDK includes comprehensive type hints for all methods:
1933
+
1934
+ ```python
1935
+ from typing import Optional
1936
+ import stytch
1937
+
1938
+ def authenticate_user(token: str) -> stytch.User:
1939
+ client = stytch.Client(
1940
+ project_id=os.getenv("STYTCH_PROJECT_ID"),
1941
+ secret=os.getenv("STYTCH_SECRET"),
1942
+ )
1943
+
1944
+ response = client.magic_links.authenticate(token=token)
1945
+ return response.user
1946
+
1947
+ def get_user_email(user: stytch.User) -> Optional[str]:
1948
+ if user.emails:
1949
+ return user.emails[0].email
1950
+ return None
1951
+ ```
1952
+
1953
+ ---
1954
+
1955
+ ## Resources
1956
+
1957
+ - **Documentation:** https://stytch.com/docs
1958
+ - **API Reference:** https://stytch.com/docs/api
1959
+ - **GitHub:** https://github.com/stytchauth/stytch-python
1960
+ - **PyPI Package:** https://pypi.org/project/stytch/
1961
+ - **Dashboard:** https://stytch.com/dashboard
1962
+ - **Support:** support@stytch.com