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.
- package/README.md +55 -0
- package/bin/chub-mcp +2 -0
- package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
- package/dist/airtable/docs/database/python/DOC.md +1735 -0
- package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
- package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
- package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
- package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
- package/dist/asana/docs/tasks/DOC.md +1396 -0
- package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
- package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
- package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
- package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
- package/dist/auth0/docs/identity/python/DOC.md +1199 -0
- package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
- package/dist/aws/docs/s3/python/DOC.md +1807 -0
- package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
- package/dist/binance/docs/trading/python/DOC.md +1454 -0
- package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
- package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
- package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
- package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
- package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
- package/dist/clerk/docs/auth/python/DOC.md +274 -0
- package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
- package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
- package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
- package/dist/cohere/docs/llm/DOC.md +1335 -0
- package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
- package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
- package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
- package/dist/deepgram/docs/speech/python/DOC.md +685 -0
- package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
- package/dist/deepl/docs/translation/python/DOC.md +944 -0
- package/dist/deepseek/docs/llm/DOC.md +1220 -0
- package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
- package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
- package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
- package/dist/discord/docs/bot/python/DOC.md +1130 -0
- package/dist/elasticsearch/docs/search/DOC.md +1634 -0
- package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
- package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
- package/dist/firebase/docs/auth/DOC.md +1015 -0
- package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
- package/dist/gemini/docs/genai/python/DOC.md +555 -0
- package/dist/github/docs/octokit/DOC.md +1560 -0
- package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
- package/dist/google/docs/bigquery/python/DOC.md +1503 -0
- package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
- package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
- package/dist/huggingface/docs/transformers/DOC.md +948 -0
- package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
- package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
- package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
- package/dist/jira/docs/issues/python/DOC.md +1492 -0
- package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
- package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
- package/dist/landingai-ade/docs/api/DOC.md +620 -0
- package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
- package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
- package/dist/landingai-ade/skills/SKILL.md +489 -0
- package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
- package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
- package/dist/linear/docs/tracker/DOC.md +1554 -0
- package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
- package/dist/livekit/docs/realtime/python/DOC.md +163 -0
- package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
- package/dist/meilisearch/docs/search/DOC.md +1241 -0
- package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
- package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
- package/dist/mongodb/docs/atlas/DOC.md +2041 -0
- package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
- package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
- package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
- package/dist/okta/docs/identity/python/DOC.md +1401 -0
- package/dist/openai/docs/chat/javascript/DOC.md +407 -0
- package/dist/openai/docs/chat/python/DOC.md +568 -0
- package/dist/paypal/docs/checkout/DOC.md +278 -0
- package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
- package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
- package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
- package/dist/plaid/docs/banking/python/DOC.md +1203 -0
- package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
- package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
- package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
- package/dist/prisma/docs/orm/python/DOC.md +1317 -0
- package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
- package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
- package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
- package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
- package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
- package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
- package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
- package/dist/redis/docs/key-value/python/DOC.md +2054 -0
- package/dist/registry.json +2817 -0
- package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
- package/dist/resend/docs/email/DOC.md +1271 -0
- package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
- package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
- package/dist/search-index.json +1 -0
- package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
- package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
- package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
- package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
- package/dist/shopify/docs/storefront/DOC.md +457 -0
- package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
- package/dist/slack/docs/workspace/python/DOC.md +271 -0
- package/dist/square/docs/payments/javascript/DOC.md +1855 -0
- package/dist/square/docs/payments/python/DOC.md +1728 -0
- package/dist/stripe/docs/api/DOC.md +1727 -0
- package/dist/stripe/docs/payments/DOC.md +1726 -0
- package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
- package/dist/stytch/docs/auth/python/DOC.md +1962 -0
- package/dist/supabase/docs/client/DOC.md +1606 -0
- package/dist/twilio/docs/messaging/python/DOC.md +469 -0
- package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
- package/dist/vercel/docs/platform/DOC.md +1940 -0
- package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
- package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
- package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
- package/dist/zendesk/docs/support/python/DOC.md +2297 -0
- package/package.json +22 -6
- package/skills/get-api-docs/SKILL.md +84 -0
- package/src/commands/annotate.js +83 -0
- package/src/commands/build.js +12 -1
- package/src/commands/feedback.js +150 -0
- package/src/commands/get.js +83 -42
- package/src/commands/search.js +7 -0
- package/src/index.js +43 -17
- package/src/lib/analytics.js +90 -0
- package/src/lib/annotations.js +57 -0
- package/src/lib/bm25.js +170 -0
- package/src/lib/cache.js +69 -6
- package/src/lib/config.js +8 -3
- package/src/lib/identity.js +99 -0
- package/src/lib/registry.js +103 -20
- package/src/lib/telemetry.js +86 -0
- package/src/mcp/server.js +177 -0
- 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
|