archastro.platform.auth
1# Copyright (c) 2026 ArchAstro Inc. Licensed under the MIT License. 2# This file is auto-generated by @archastro/sdk-generator. Do not edit. 3# Content hash: 4cece1e1b7b8 4 5from __future__ import annotations 6 7from dataclasses import dataclass 8 9from .runtime.http_client import HttpClient, SyncHttpClient 10 11 12@dataclass 13class AuthTokens: 14 token_expiry: int | None = None 15 refresh_token: str | None = None 16 access_token: str | None = None 17 18 19class AsyncAuthClient: 20 def __init__(self, http: HttpClient): 21 self._http = http 22 23 async def allowed_auth_methods(self) -> dict: 24 """ 25 List supported auth methods 26 Returns the complete catalogue of authentication methods the platform supports, 27 including each method's stable slug, user-facing name, and description. 28 Use this endpoint to render method labels in sign-in UIs or org settings screens 29 without hardcoding copy or maintaining your own enum list. Results reflect the 30 platform's source-of-truth catalogue and are consistent across all orgs. 31 This endpoint requires only a publishable key and is accessible without an active 32 user session, making it suitable for pre-authentication flows such as login page 33 rendering or onboarding configuration. 34 35 Returns: 36 Successful response 37 """ 38 data = await self._http.request( 39 "/api/v1/auth/allowed_auth_methods", 40 method="GET", 41 ) 42 return data 43 44 async def login(self, email: str, password: str) -> AuthTokens: 45 """ 46 Authenticate with email and password 47 Authenticates a user with an email address and password and returns a short-lived 48 access token, a refresh token, and the authenticated user object. Use the refresh 49 token with the `/auth/refresh` endpoint to obtain new access tokens without 50 re-authenticating. 51 Password login must be enabled for the app; apps that have disabled password 52 authentication return HTTP 403. Requests are rate-limited per IP (10 per minute) 53 and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429. 54 55 Args: 56 email: Email address of the user to authenticate. 57 password: Password for the account associated with the given email. 58 59 Returns: 60 Access token, refresh token, and authenticated user object. 61 """ 62 body: dict[str, object] = {} 63 body["email"] = email 64 body["password"] = password 65 66 data = await self._http.request( 67 "/api/v1/auth/login", 68 method="POST", 69 body=body, 70 ) 71 return AuthTokens( 72 token_expiry=data.get("expires_in"), 73 refresh_token=data.get("refresh_token"), 74 access_token=data.get("token"), 75 ) 76 77 async def request_login_magic_link( 78 self, email: str | None = None, redirect_uri: str | None = None 79 ) -> dict: 80 """ 81 Request a magic link for login 82 Sends a magic link to the given email address so an existing user can sign in 83 without a password. The user clicks the link in their email and is redirected to 84 `redirect_uri` with a token; pass that token to `/api/v1/auth/verify/link` to obtain 85 session tokens. 86 If no account exists for the email, the endpoint still returns success to prevent 87 email enumeration no link is sent in that case. Both `email` and `redirect_uri` 88 are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair 89 (3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success. 90 91 Args: 92 email: Email address of the account to send the magic link to. 93 redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter. 94 95 Returns: 96 No content 97 """ 98 body: dict[str, object] = {} 99 if email is not None: 100 body["email"] = email 101 if redirect_uri is not None: 102 body["redirect_uri"] = redirect_uri 103 104 data = await self._http.request( 105 "/api/v1/auth/login/link", 106 method="POST", 107 body=body, 108 ) 109 return data 110 111 async def refresh(self, refresh_token: str) -> AuthTokens: 112 """ 113 Refresh an access token 114 Exchanges a valid refresh token for a new access token and a new refresh token, 115 rotating the refresh token on every call. The response also includes the updated 116 user object. Store the new refresh token and discard the old one. 117 Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. 118 Rate limiting is applied per (user, IP) pair when the token can be verified, and 119 falls back to IP-only when it cannot. The limit is 30 exchanges per minute per 120 bucket; exceeding it returns HTTP 429. 121 122 Args: 123 refresh_token: Refresh token previously issued by a login, registration, or token-refresh response. 124 125 Returns: 126 New access token, new refresh token, and the authenticated user object. 127 """ 128 body: dict[str, object] = {} 129 body["refresh_token"] = refresh_token 130 131 data = await self._http.request( 132 "/api/v1/auth/refresh", 133 method="POST", 134 body=body, 135 ) 136 return AuthTokens( 137 token_expiry=data.get("expires_in"), 138 refresh_token=data.get("refresh_token"), 139 access_token=data.get("token"), 140 ) 141 142 async def register( 143 self, 144 email: str, 145 alias: str | None = None, 146 full_name: str | None = None, 147 invite_code: str | None = None, 148 password: str | None = None, 149 set_org: str | None = None, 150 team_invite: str | None = None, 151 timezone: str | None = None, 152 ) -> AuthTokens: 153 """ 154 Register a new user with email and password 155 Creates a new user account and returns an access token, refresh token, and the new 156 user object. Two registration paths are supported: 157 - **Team registration**: supply `team_invite` with a valid team invite ID. The new 158 user is added to that team immediately upon registration. Returns HTTP 404 if the 159 invite is not found. 160 - **Standard registration**: supply `password`. An `invite_code` may optionally be 161 included for invite-gated apps; an invalid code returns HTTP 404. 162 Provide `team_invite` or `password`; when both are supplied, team registration 163 takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 164 with field-level guidance. Use `full_name` for the profile name (returned as 165 `user.name`); `name` is not an input field. `alias` is a separate optional handle. 166 Legacy `team_invite_id` remains accepted; `team_invite` takes precedence. 167 Password registration must be enabled for the app; disabled apps return 168 HTTP 403. The response status is HTTP 201 on success. 169 170 Args: 171 email: Email address for the new account. 172 alias: Display alias (handle) for the new account. 173 full_name: Full name for the new account. 174 invite_code: Invite code for invite-gated registration. Applied only in the standard registration path. 175 password: Password for the new account. Required for standard (non-team-invite) registration. 176 set_org: Create or reuse an organization from the work-email domain and stamp the new user into it. 177 team_invite: Team invite ID. When provided, the user is added to the team on registration. 178 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 179 180 Returns: 181 Access token, refresh token, and the newly created user object. 182 """ 183 body: dict[str, object] = {} 184 body["email"] = email 185 if alias is not None: 186 body["alias"] = alias 187 if full_name is not None: 188 body["full_name"] = full_name 189 if invite_code is not None: 190 body["invite_code"] = invite_code 191 if password is not None: 192 body["password"] = password 193 if set_org is not None: 194 body["set_org"] = set_org 195 if team_invite is not None: 196 body["team_invite"] = team_invite 197 if timezone is not None: 198 body["timezone"] = timezone 199 200 data = await self._http.request( 201 "/api/v1/auth/register", 202 method="POST", 203 body=body, 204 ) 205 return AuthTokens( 206 token_expiry=data.get("expires_in"), 207 refresh_token=data.get("refresh_token"), 208 access_token=data.get("token"), 209 ) 210 211 async def request_register_magic_link( 212 self, 213 alias: str | None = None, 214 email: str | None = None, 215 full_name: str | None = None, 216 org: str | None = None, 217 redirect_uri: str | None = None, 218 set_org: str | None = None, 219 timezone: str | None = None, 220 ) -> dict: 221 """ 222 Request a magic link for registration 223 Starts a passwordless registration flow by sending a verification link to the given 224 email address. The recipient clicks the link and is redirected to `redirect_uri` with 225 a token; pass that token to `/api/v1/auth/verify/link` to complete registration and obtain 226 session tokens. 227 Profile fields (`full_name`, `alias`, `timezone`) and, with `set_org`, the `org` 228 details are captured now and applied when the link is verified; nothing is created 229 before the click. Requests are rate-limited per IP (10 per minute) and per 230 email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns 231 HTTP 204 on success. 232 233 Args: 234 alias: Display alias (handle) for the new account. 235 email: Email address to send the registration magic link to. 236 full_name: Full name for the new account. 237 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 238 redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter. 239 set_org: Create or reuse an organization from the work-email domain during confirmation. 240 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 241 242 Returns: 243 No content 244 """ 245 body: dict[str, object] = {} 246 if alias is not None: 247 body["alias"] = alias 248 if email is not None: 249 body["email"] = email 250 if full_name is not None: 251 body["full_name"] = full_name 252 if org is not None: 253 body["org"] = org 254 if redirect_uri is not None: 255 body["redirect_uri"] = redirect_uri 256 if set_org is not None: 257 body["set_org"] = set_org 258 if timezone is not None: 259 body["timezone"] = timezone 260 261 data = await self._http.request( 262 "/api/v1/auth/register/link", 263 method="POST", 264 body=body, 265 ) 266 return data 267 268 async def request_magic_link( 269 self, 270 email: str | None = None, 271 full_name: str | None = None, 272 mcp_client_id: str | None = None, 273 org: str | None = None, 274 redirect_uri: str | None = None, 275 set_org: str | None = None, 276 signup_origin: str | None = None, 277 ) -> dict: 278 """ 279 Request a magic link for login or registration 280 Sends a passwordless magic link to the given email address. If an account with that 281 email already exists, a login link is sent. If no account exists, a registration link 282 is sent and the recipient completes sign-up by clicking through. This unified endpoint 283 lets you implement a single email-entry UI that handles both cases transparently. 284 The `redirect_uri` is validated against the app's registered hosts; an unregistered 285 URI returns HTTP 400. Both `email` and `redirect_uri` are required. Requests are 286 rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns 287 HTTP 204 on success no body. If the app has Allowed Users enabled and the 288 address is not on the list, returns HTTP 403 `user_not_allowed`. That refusal 289 is the same for new and existing addresses, so it does not reveal whether an 290 account exists. 291 For a new user, `full_name` and (with `set_org`) the `org` details ride the pending 292 registration and are applied when the link is verified; nothing is created before 293 the click. They are ignored when the email already belongs to a user. Malformed 294 values return HTTP 422 before the address is looked up, so the response cannot 295 reveal whether an account exists. 296 297 Args: 298 email: Email address to send the magic link to. 299 full_name: Display name for the account the verified link will create. 300 mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key. 301 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 302 redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app. 303 set_org: For a new user, create or reuse an organization from the work-email domain during confirmation. 304 signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication. 305 306 Returns: 307 No content 308 """ 309 body: dict[str, object] = {} 310 if email is not None: 311 body["email"] = email 312 if full_name is not None: 313 body["full_name"] = full_name 314 if mcp_client_id is not None: 315 body["mcp_client_id"] = mcp_client_id 316 if org is not None: 317 body["org"] = org 318 if redirect_uri is not None: 319 body["redirect_uri"] = redirect_uri 320 if set_org is not None: 321 body["set_org"] = set_org 322 if signup_origin is not None: 323 body["signup_origin"] = signup_origin 324 325 data = await self._http.request( 326 "/api/v1/auth/request/link", 327 method="POST", 328 body=body, 329 ) 330 return data 331 332 async def exchange_login_token(self, token: str, timezone: str | None = None) -> AuthTokens: 333 """ 334 Exchange a one-time login token for session tokens 335 Exchanges an email-login token (including email reply links) or an internal 336 impersonation login token for session credentials. Passwordless magic-link tokens 337 from `/api/v1/auth/login/link`, `/api/v1/auth/register/link`, or 338 `/api/v1/auth/request/link` must instead go to `/api/v1/auth/verify/link`. 339 Numeric email codes go to `/api/v1/auth/code/verify` with `email` and `code`. 340 Access and refresh tokens are not accepted here. Invalid, expired, or already-used 341 login tokens return HTTP 401 with structured `error.type`, `error.code`, and 342 `error.message` fields. 343 If `timezone` is provided and the user's current timezone is still the default 344 (`"America/Los_Angeles"`), the account timezone is updated in the same request. 345 Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429. 346 347 Args: 348 token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code. 349 timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g. `"Europe/London"`. Omit to leave the timezone unchanged. 350 351 Returns: 352 Access token, refresh token, and the authenticated user object. 353 """ 354 body: dict[str, object] = {} 355 body["token"] = token 356 if timezone is not None: 357 body["timezone"] = timezone 358 359 data = await self._http.request( 360 "/api/v1/auth/token", 361 method="POST", 362 body=body, 363 ) 364 return AuthTokens( 365 token_expiry=data.get("expires_in"), 366 refresh_token=data.get("refresh_token"), 367 access_token=data.get("token"), 368 ) 369 370 async def verify_magic_link(self, token: str | None = None) -> AuthTokens: 371 """ 372 Verify a magic link token 373 Consumes a single-use token from a magic link URL and returns an access token, 374 refresh token, and the authenticated user object. This endpoint completes both the 375 login flow (initiated by `/api/v1/auth/login/link`) and the registration flow 376 (initiated by `/api/v1/auth/register/link` or `/api/v1/auth/request/link`). 377 Extract the token from the `token` query parameter of the magic link redirect URI 378 and POST JSON `{"token":"<token from redirect URI>"}` to 379 `/api/v1/auth/verify/link`, not `/api/v1/auth/token`. Expired or already-used tokens return HTTP 401 expired links 380 have `error.code` = `authentication_error` and `error.message` = 381 `expired_token`; unknown or already-used tokens have `error.message` = 382 `invalid_or_expired_token`. If the app has disabled passwordless authentication 383 the request returns HTTP 403. Rate-limited to 10 requests per IP per minute 384 exceeding this returns HTTP 429. 385 386 Args: 387 token: Single-use magic link token extracted from the redirect URI query parameter. 388 389 Returns: 390 Access token, refresh token, and the authenticated user object. 391 """ 392 body: dict[str, object] = {} 393 if token is not None: 394 body["token"] = token 395 396 data = await self._http.request( 397 "/api/v1/auth/verify/link", 398 method="POST", 399 body=body, 400 ) 401 return AuthTokens( 402 token_expiry=data.get("expires_in"), 403 refresh_token=data.get("refresh_token"), 404 access_token=data.get("token"), 405 ) 406 407 408class AuthClient: 409 def __init__(self, http: SyncHttpClient): 410 self._http = http 411 412 def allowed_auth_methods(self) -> dict: 413 """ 414 List supported auth methods 415 Returns the complete catalogue of authentication methods the platform supports, 416 including each method's stable slug, user-facing name, and description. 417 Use this endpoint to render method labels in sign-in UIs or org settings screens 418 without hardcoding copy or maintaining your own enum list. Results reflect the 419 platform's source-of-truth catalogue and are consistent across all orgs. 420 This endpoint requires only a publishable key and is accessible without an active 421 user session, making it suitable for pre-authentication flows such as login page 422 rendering or onboarding configuration. 423 424 Returns: 425 Successful response 426 """ 427 data = self._http.request( 428 "/api/v1/auth/allowed_auth_methods", 429 method="GET", 430 ) 431 return data 432 433 def login(self, email: str, password: str) -> AuthTokens: 434 """ 435 Authenticate with email and password 436 Authenticates a user with an email address and password and returns a short-lived 437 access token, a refresh token, and the authenticated user object. Use the refresh 438 token with the `/auth/refresh` endpoint to obtain new access tokens without 439 re-authenticating. 440 Password login must be enabled for the app; apps that have disabled password 441 authentication return HTTP 403. Requests are rate-limited per IP (10 per minute) 442 and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429. 443 444 Args: 445 email: Email address of the user to authenticate. 446 password: Password for the account associated with the given email. 447 448 Returns: 449 Access token, refresh token, and authenticated user object. 450 """ 451 body: dict[str, object] = {} 452 body["email"] = email 453 body["password"] = password 454 455 data = self._http.request( 456 "/api/v1/auth/login", 457 method="POST", 458 body=body, 459 ) 460 return AuthTokens( 461 token_expiry=data.get("expires_in"), 462 refresh_token=data.get("refresh_token"), 463 access_token=data.get("token"), 464 ) 465 466 def request_login_magic_link( 467 self, email: str | None = None, redirect_uri: str | None = None 468 ) -> dict: 469 """ 470 Request a magic link for login 471 Sends a magic link to the given email address so an existing user can sign in 472 without a password. The user clicks the link in their email and is redirected to 473 `redirect_uri` with a token; pass that token to `/api/v1/auth/verify/link` to obtain 474 session tokens. 475 If no account exists for the email, the endpoint still returns success to prevent 476 email enumeration no link is sent in that case. Both `email` and `redirect_uri` 477 are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair 478 (3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success. 479 480 Args: 481 email: Email address of the account to send the magic link to. 482 redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter. 483 484 Returns: 485 No content 486 """ 487 body: dict[str, object] = {} 488 if email is not None: 489 body["email"] = email 490 if redirect_uri is not None: 491 body["redirect_uri"] = redirect_uri 492 493 data = self._http.request( 494 "/api/v1/auth/login/link", 495 method="POST", 496 body=body, 497 ) 498 return data 499 500 def refresh(self, refresh_token: str) -> AuthTokens: 501 """ 502 Refresh an access token 503 Exchanges a valid refresh token for a new access token and a new refresh token, 504 rotating the refresh token on every call. The response also includes the updated 505 user object. Store the new refresh token and discard the old one. 506 Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. 507 Rate limiting is applied per (user, IP) pair when the token can be verified, and 508 falls back to IP-only when it cannot. The limit is 30 exchanges per minute per 509 bucket; exceeding it returns HTTP 429. 510 511 Args: 512 refresh_token: Refresh token previously issued by a login, registration, or token-refresh response. 513 514 Returns: 515 New access token, new refresh token, and the authenticated user object. 516 """ 517 body: dict[str, object] = {} 518 body["refresh_token"] = refresh_token 519 520 data = self._http.request( 521 "/api/v1/auth/refresh", 522 method="POST", 523 body=body, 524 ) 525 return AuthTokens( 526 token_expiry=data.get("expires_in"), 527 refresh_token=data.get("refresh_token"), 528 access_token=data.get("token"), 529 ) 530 531 def register( 532 self, 533 email: str, 534 alias: str | None = None, 535 full_name: str | None = None, 536 invite_code: str | None = None, 537 password: str | None = None, 538 set_org: str | None = None, 539 team_invite: str | None = None, 540 timezone: str | None = None, 541 ) -> AuthTokens: 542 """ 543 Register a new user with email and password 544 Creates a new user account and returns an access token, refresh token, and the new 545 user object. Two registration paths are supported: 546 - **Team registration**: supply `team_invite` with a valid team invite ID. The new 547 user is added to that team immediately upon registration. Returns HTTP 404 if the 548 invite is not found. 549 - **Standard registration**: supply `password`. An `invite_code` may optionally be 550 included for invite-gated apps; an invalid code returns HTTP 404. 551 Provide `team_invite` or `password`; when both are supplied, team registration 552 takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 553 with field-level guidance. Use `full_name` for the profile name (returned as 554 `user.name`); `name` is not an input field. `alias` is a separate optional handle. 555 Legacy `team_invite_id` remains accepted; `team_invite` takes precedence. 556 Password registration must be enabled for the app; disabled apps return 557 HTTP 403. The response status is HTTP 201 on success. 558 559 Args: 560 email: Email address for the new account. 561 alias: Display alias (handle) for the new account. 562 full_name: Full name for the new account. 563 invite_code: Invite code for invite-gated registration. Applied only in the standard registration path. 564 password: Password for the new account. Required for standard (non-team-invite) registration. 565 set_org: Create or reuse an organization from the work-email domain and stamp the new user into it. 566 team_invite: Team invite ID. When provided, the user is added to the team on registration. 567 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 568 569 Returns: 570 Access token, refresh token, and the newly created user object. 571 """ 572 body: dict[str, object] = {} 573 body["email"] = email 574 if alias is not None: 575 body["alias"] = alias 576 if full_name is not None: 577 body["full_name"] = full_name 578 if invite_code is not None: 579 body["invite_code"] = invite_code 580 if password is not None: 581 body["password"] = password 582 if set_org is not None: 583 body["set_org"] = set_org 584 if team_invite is not None: 585 body["team_invite"] = team_invite 586 if timezone is not None: 587 body["timezone"] = timezone 588 589 data = self._http.request( 590 "/api/v1/auth/register", 591 method="POST", 592 body=body, 593 ) 594 return AuthTokens( 595 token_expiry=data.get("expires_in"), 596 refresh_token=data.get("refresh_token"), 597 access_token=data.get("token"), 598 ) 599 600 def request_register_magic_link( 601 self, 602 alias: str | None = None, 603 email: str | None = None, 604 full_name: str | None = None, 605 org: str | None = None, 606 redirect_uri: str | None = None, 607 set_org: str | None = None, 608 timezone: str | None = None, 609 ) -> dict: 610 """ 611 Request a magic link for registration 612 Starts a passwordless registration flow by sending a verification link to the given 613 email address. The recipient clicks the link and is redirected to `redirect_uri` with 614 a token; pass that token to `/api/v1/auth/verify/link` to complete registration and obtain 615 session tokens. 616 Profile fields (`full_name`, `alias`, `timezone`) and, with `set_org`, the `org` 617 details are captured now and applied when the link is verified; nothing is created 618 before the click. Requests are rate-limited per IP (10 per minute) and per 619 email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns 620 HTTP 204 on success. 621 622 Args: 623 alias: Display alias (handle) for the new account. 624 email: Email address to send the registration magic link to. 625 full_name: Full name for the new account. 626 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 627 redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter. 628 set_org: Create or reuse an organization from the work-email domain during confirmation. 629 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 630 631 Returns: 632 No content 633 """ 634 body: dict[str, object] = {} 635 if alias is not None: 636 body["alias"] = alias 637 if email is not None: 638 body["email"] = email 639 if full_name is not None: 640 body["full_name"] = full_name 641 if org is not None: 642 body["org"] = org 643 if redirect_uri is not None: 644 body["redirect_uri"] = redirect_uri 645 if set_org is not None: 646 body["set_org"] = set_org 647 if timezone is not None: 648 body["timezone"] = timezone 649 650 data = self._http.request( 651 "/api/v1/auth/register/link", 652 method="POST", 653 body=body, 654 ) 655 return data 656 657 def request_magic_link( 658 self, 659 email: str | None = None, 660 full_name: str | None = None, 661 mcp_client_id: str | None = None, 662 org: str | None = None, 663 redirect_uri: str | None = None, 664 set_org: str | None = None, 665 signup_origin: str | None = None, 666 ) -> dict: 667 """ 668 Request a magic link for login or registration 669 Sends a passwordless magic link to the given email address. If an account with that 670 email already exists, a login link is sent. If no account exists, a registration link 671 is sent and the recipient completes sign-up by clicking through. This unified endpoint 672 lets you implement a single email-entry UI that handles both cases transparently. 673 The `redirect_uri` is validated against the app's registered hosts; an unregistered 674 URI returns HTTP 400. Both `email` and `redirect_uri` are required. Requests are 675 rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns 676 HTTP 204 on success no body. If the app has Allowed Users enabled and the 677 address is not on the list, returns HTTP 403 `user_not_allowed`. That refusal 678 is the same for new and existing addresses, so it does not reveal whether an 679 account exists. 680 For a new user, `full_name` and (with `set_org`) the `org` details ride the pending 681 registration and are applied when the link is verified; nothing is created before 682 the click. They are ignored when the email already belongs to a user. Malformed 683 values return HTTP 422 before the address is looked up, so the response cannot 684 reveal whether an account exists. 685 686 Args: 687 email: Email address to send the magic link to. 688 full_name: Display name for the account the verified link will create. 689 mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key. 690 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 691 redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app. 692 set_org: For a new user, create or reuse an organization from the work-email domain during confirmation. 693 signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication. 694 695 Returns: 696 No content 697 """ 698 body: dict[str, object] = {} 699 if email is not None: 700 body["email"] = email 701 if full_name is not None: 702 body["full_name"] = full_name 703 if mcp_client_id is not None: 704 body["mcp_client_id"] = mcp_client_id 705 if org is not None: 706 body["org"] = org 707 if redirect_uri is not None: 708 body["redirect_uri"] = redirect_uri 709 if set_org is not None: 710 body["set_org"] = set_org 711 if signup_origin is not None: 712 body["signup_origin"] = signup_origin 713 714 data = self._http.request( 715 "/api/v1/auth/request/link", 716 method="POST", 717 body=body, 718 ) 719 return data 720 721 def exchange_login_token(self, token: str, timezone: str | None = None) -> AuthTokens: 722 """ 723 Exchange a one-time login token for session tokens 724 Exchanges an email-login token (including email reply links) or an internal 725 impersonation login token for session credentials. Passwordless magic-link tokens 726 from `/api/v1/auth/login/link`, `/api/v1/auth/register/link`, or 727 `/api/v1/auth/request/link` must instead go to `/api/v1/auth/verify/link`. 728 Numeric email codes go to `/api/v1/auth/code/verify` with `email` and `code`. 729 Access and refresh tokens are not accepted here. Invalid, expired, or already-used 730 login tokens return HTTP 401 with structured `error.type`, `error.code`, and 731 `error.message` fields. 732 If `timezone` is provided and the user's current timezone is still the default 733 (`"America/Los_Angeles"`), the account timezone is updated in the same request. 734 Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429. 735 736 Args: 737 token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code. 738 timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g. `"Europe/London"`. Omit to leave the timezone unchanged. 739 740 Returns: 741 Access token, refresh token, and the authenticated user object. 742 """ 743 body: dict[str, object] = {} 744 body["token"] = token 745 if timezone is not None: 746 body["timezone"] = timezone 747 748 data = self._http.request( 749 "/api/v1/auth/token", 750 method="POST", 751 body=body, 752 ) 753 return AuthTokens( 754 token_expiry=data.get("expires_in"), 755 refresh_token=data.get("refresh_token"), 756 access_token=data.get("token"), 757 ) 758 759 def verify_magic_link(self, token: str | None = None) -> AuthTokens: 760 """ 761 Verify a magic link token 762 Consumes a single-use token from a magic link URL and returns an access token, 763 refresh token, and the authenticated user object. This endpoint completes both the 764 login flow (initiated by `/api/v1/auth/login/link`) and the registration flow 765 (initiated by `/api/v1/auth/register/link` or `/api/v1/auth/request/link`). 766 Extract the token from the `token` query parameter of the magic link redirect URI 767 and POST JSON `{"token":"<token from redirect URI>"}` to 768 `/api/v1/auth/verify/link`, not `/api/v1/auth/token`. Expired or already-used tokens return HTTP 401 expired links 769 have `error.code` = `authentication_error` and `error.message` = 770 `expired_token`; unknown or already-used tokens have `error.message` = 771 `invalid_or_expired_token`. If the app has disabled passwordless authentication 772 the request returns HTTP 403. Rate-limited to 10 requests per IP per minute 773 exceeding this returns HTTP 429. 774 775 Args: 776 token: Single-use magic link token extracted from the redirect URI query parameter. 777 778 Returns: 779 Access token, refresh token, and the authenticated user object. 780 """ 781 body: dict[str, object] = {} 782 if token is not None: 783 body["token"] = token 784 785 data = self._http.request( 786 "/api/v1/auth/verify/link", 787 method="POST", 788 body=body, 789 ) 790 return AuthTokens( 791 token_expiry=data.get("expires_in"), 792 refresh_token=data.get("refresh_token"), 793 access_token=data.get("token"), 794 )
13@dataclass 14class AuthTokens: 15 token_expiry: int | None = None 16 refresh_token: str | None = None 17 access_token: str | None = None
20class AsyncAuthClient: 21 def __init__(self, http: HttpClient): 22 self._http = http 23 24 async def allowed_auth_methods(self) -> dict: 25 """ 26 List supported auth methods 27 Returns the complete catalogue of authentication methods the platform supports, 28 including each method's stable slug, user-facing name, and description. 29 Use this endpoint to render method labels in sign-in UIs or org settings screens 30 without hardcoding copy or maintaining your own enum list. Results reflect the 31 platform's source-of-truth catalogue and are consistent across all orgs. 32 This endpoint requires only a publishable key and is accessible without an active 33 user session, making it suitable for pre-authentication flows such as login page 34 rendering or onboarding configuration. 35 36 Returns: 37 Successful response 38 """ 39 data = await self._http.request( 40 "/api/v1/auth/allowed_auth_methods", 41 method="GET", 42 ) 43 return data 44 45 async def login(self, email: str, password: str) -> AuthTokens: 46 """ 47 Authenticate with email and password 48 Authenticates a user with an email address and password and returns a short-lived 49 access token, a refresh token, and the authenticated user object. Use the refresh 50 token with the `/auth/refresh` endpoint to obtain new access tokens without 51 re-authenticating. 52 Password login must be enabled for the app; apps that have disabled password 53 authentication return HTTP 403. Requests are rate-limited per IP (10 per minute) 54 and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429. 55 56 Args: 57 email: Email address of the user to authenticate. 58 password: Password for the account associated with the given email. 59 60 Returns: 61 Access token, refresh token, and authenticated user object. 62 """ 63 body: dict[str, object] = {} 64 body["email"] = email 65 body["password"] = password 66 67 data = await self._http.request( 68 "/api/v1/auth/login", 69 method="POST", 70 body=body, 71 ) 72 return AuthTokens( 73 token_expiry=data.get("expires_in"), 74 refresh_token=data.get("refresh_token"), 75 access_token=data.get("token"), 76 ) 77 78 async def request_login_magic_link( 79 self, email: str | None = None, redirect_uri: str | None = None 80 ) -> dict: 81 """ 82 Request a magic link for login 83 Sends a magic link to the given email address so an existing user can sign in 84 without a password. The user clicks the link in their email and is redirected to 85 `redirect_uri` with a token; pass that token to `/api/v1/auth/verify/link` to obtain 86 session tokens. 87 If no account exists for the email, the endpoint still returns success to prevent 88 email enumeration no link is sent in that case. Both `email` and `redirect_uri` 89 are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair 90 (3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success. 91 92 Args: 93 email: Email address of the account to send the magic link to. 94 redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter. 95 96 Returns: 97 No content 98 """ 99 body: dict[str, object] = {} 100 if email is not None: 101 body["email"] = email 102 if redirect_uri is not None: 103 body["redirect_uri"] = redirect_uri 104 105 data = await self._http.request( 106 "/api/v1/auth/login/link", 107 method="POST", 108 body=body, 109 ) 110 return data 111 112 async def refresh(self, refresh_token: str) -> AuthTokens: 113 """ 114 Refresh an access token 115 Exchanges a valid refresh token for a new access token and a new refresh token, 116 rotating the refresh token on every call. The response also includes the updated 117 user object. Store the new refresh token and discard the old one. 118 Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. 119 Rate limiting is applied per (user, IP) pair when the token can be verified, and 120 falls back to IP-only when it cannot. The limit is 30 exchanges per minute per 121 bucket; exceeding it returns HTTP 429. 122 123 Args: 124 refresh_token: Refresh token previously issued by a login, registration, or token-refresh response. 125 126 Returns: 127 New access token, new refresh token, and the authenticated user object. 128 """ 129 body: dict[str, object] = {} 130 body["refresh_token"] = refresh_token 131 132 data = await self._http.request( 133 "/api/v1/auth/refresh", 134 method="POST", 135 body=body, 136 ) 137 return AuthTokens( 138 token_expiry=data.get("expires_in"), 139 refresh_token=data.get("refresh_token"), 140 access_token=data.get("token"), 141 ) 142 143 async def register( 144 self, 145 email: str, 146 alias: str | None = None, 147 full_name: str | None = None, 148 invite_code: str | None = None, 149 password: str | None = None, 150 set_org: str | None = None, 151 team_invite: str | None = None, 152 timezone: str | None = None, 153 ) -> AuthTokens: 154 """ 155 Register a new user with email and password 156 Creates a new user account and returns an access token, refresh token, and the new 157 user object. Two registration paths are supported: 158 - **Team registration**: supply `team_invite` with a valid team invite ID. The new 159 user is added to that team immediately upon registration. Returns HTTP 404 if the 160 invite is not found. 161 - **Standard registration**: supply `password`. An `invite_code` may optionally be 162 included for invite-gated apps; an invalid code returns HTTP 404. 163 Provide `team_invite` or `password`; when both are supplied, team registration 164 takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 165 with field-level guidance. Use `full_name` for the profile name (returned as 166 `user.name`); `name` is not an input field. `alias` is a separate optional handle. 167 Legacy `team_invite_id` remains accepted; `team_invite` takes precedence. 168 Password registration must be enabled for the app; disabled apps return 169 HTTP 403. The response status is HTTP 201 on success. 170 171 Args: 172 email: Email address for the new account. 173 alias: Display alias (handle) for the new account. 174 full_name: Full name for the new account. 175 invite_code: Invite code for invite-gated registration. Applied only in the standard registration path. 176 password: Password for the new account. Required for standard (non-team-invite) registration. 177 set_org: Create or reuse an organization from the work-email domain and stamp the new user into it. 178 team_invite: Team invite ID. When provided, the user is added to the team on registration. 179 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 180 181 Returns: 182 Access token, refresh token, and the newly created user object. 183 """ 184 body: dict[str, object] = {} 185 body["email"] = email 186 if alias is not None: 187 body["alias"] = alias 188 if full_name is not None: 189 body["full_name"] = full_name 190 if invite_code is not None: 191 body["invite_code"] = invite_code 192 if password is not None: 193 body["password"] = password 194 if set_org is not None: 195 body["set_org"] = set_org 196 if team_invite is not None: 197 body["team_invite"] = team_invite 198 if timezone is not None: 199 body["timezone"] = timezone 200 201 data = await self._http.request( 202 "/api/v1/auth/register", 203 method="POST", 204 body=body, 205 ) 206 return AuthTokens( 207 token_expiry=data.get("expires_in"), 208 refresh_token=data.get("refresh_token"), 209 access_token=data.get("token"), 210 ) 211 212 async def request_register_magic_link( 213 self, 214 alias: str | None = None, 215 email: str | None = None, 216 full_name: str | None = None, 217 org: str | None = None, 218 redirect_uri: str | None = None, 219 set_org: str | None = None, 220 timezone: str | None = None, 221 ) -> dict: 222 """ 223 Request a magic link for registration 224 Starts a passwordless registration flow by sending a verification link to the given 225 email address. The recipient clicks the link and is redirected to `redirect_uri` with 226 a token; pass that token to `/api/v1/auth/verify/link` to complete registration and obtain 227 session tokens. 228 Profile fields (`full_name`, `alias`, `timezone`) and, with `set_org`, the `org` 229 details are captured now and applied when the link is verified; nothing is created 230 before the click. Requests are rate-limited per IP (10 per minute) and per 231 email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns 232 HTTP 204 on success. 233 234 Args: 235 alias: Display alias (handle) for the new account. 236 email: Email address to send the registration magic link to. 237 full_name: Full name for the new account. 238 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 239 redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter. 240 set_org: Create or reuse an organization from the work-email domain during confirmation. 241 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 242 243 Returns: 244 No content 245 """ 246 body: dict[str, object] = {} 247 if alias is not None: 248 body["alias"] = alias 249 if email is not None: 250 body["email"] = email 251 if full_name is not None: 252 body["full_name"] = full_name 253 if org is not None: 254 body["org"] = org 255 if redirect_uri is not None: 256 body["redirect_uri"] = redirect_uri 257 if set_org is not None: 258 body["set_org"] = set_org 259 if timezone is not None: 260 body["timezone"] = timezone 261 262 data = await self._http.request( 263 "/api/v1/auth/register/link", 264 method="POST", 265 body=body, 266 ) 267 return data 268 269 async def request_magic_link( 270 self, 271 email: str | None = None, 272 full_name: str | None = None, 273 mcp_client_id: str | None = None, 274 org: str | None = None, 275 redirect_uri: str | None = None, 276 set_org: str | None = None, 277 signup_origin: str | None = None, 278 ) -> dict: 279 """ 280 Request a magic link for login or registration 281 Sends a passwordless magic link to the given email address. If an account with that 282 email already exists, a login link is sent. If no account exists, a registration link 283 is sent and the recipient completes sign-up by clicking through. This unified endpoint 284 lets you implement a single email-entry UI that handles both cases transparently. 285 The `redirect_uri` is validated against the app's registered hosts; an unregistered 286 URI returns HTTP 400. Both `email` and `redirect_uri` are required. Requests are 287 rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns 288 HTTP 204 on success no body. If the app has Allowed Users enabled and the 289 address is not on the list, returns HTTP 403 `user_not_allowed`. That refusal 290 is the same for new and existing addresses, so it does not reveal whether an 291 account exists. 292 For a new user, `full_name` and (with `set_org`) the `org` details ride the pending 293 registration and are applied when the link is verified; nothing is created before 294 the click. They are ignored when the email already belongs to a user. Malformed 295 values return HTTP 422 before the address is looked up, so the response cannot 296 reveal whether an account exists. 297 298 Args: 299 email: Email address to send the magic link to. 300 full_name: Display name for the account the verified link will create. 301 mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key. 302 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 303 redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app. 304 set_org: For a new user, create or reuse an organization from the work-email domain during confirmation. 305 signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication. 306 307 Returns: 308 No content 309 """ 310 body: dict[str, object] = {} 311 if email is not None: 312 body["email"] = email 313 if full_name is not None: 314 body["full_name"] = full_name 315 if mcp_client_id is not None: 316 body["mcp_client_id"] = mcp_client_id 317 if org is not None: 318 body["org"] = org 319 if redirect_uri is not None: 320 body["redirect_uri"] = redirect_uri 321 if set_org is not None: 322 body["set_org"] = set_org 323 if signup_origin is not None: 324 body["signup_origin"] = signup_origin 325 326 data = await self._http.request( 327 "/api/v1/auth/request/link", 328 method="POST", 329 body=body, 330 ) 331 return data 332 333 async def exchange_login_token(self, token: str, timezone: str | None = None) -> AuthTokens: 334 """ 335 Exchange a one-time login token for session tokens 336 Exchanges an email-login token (including email reply links) or an internal 337 impersonation login token for session credentials. Passwordless magic-link tokens 338 from `/api/v1/auth/login/link`, `/api/v1/auth/register/link`, or 339 `/api/v1/auth/request/link` must instead go to `/api/v1/auth/verify/link`. 340 Numeric email codes go to `/api/v1/auth/code/verify` with `email` and `code`. 341 Access and refresh tokens are not accepted here. Invalid, expired, or already-used 342 login tokens return HTTP 401 with structured `error.type`, `error.code`, and 343 `error.message` fields. 344 If `timezone` is provided and the user's current timezone is still the default 345 (`"America/Los_Angeles"`), the account timezone is updated in the same request. 346 Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429. 347 348 Args: 349 token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code. 350 timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g. `"Europe/London"`. Omit to leave the timezone unchanged. 351 352 Returns: 353 Access token, refresh token, and the authenticated user object. 354 """ 355 body: dict[str, object] = {} 356 body["token"] = token 357 if timezone is not None: 358 body["timezone"] = timezone 359 360 data = await self._http.request( 361 "/api/v1/auth/token", 362 method="POST", 363 body=body, 364 ) 365 return AuthTokens( 366 token_expiry=data.get("expires_in"), 367 refresh_token=data.get("refresh_token"), 368 access_token=data.get("token"), 369 ) 370 371 async def verify_magic_link(self, token: str | None = None) -> AuthTokens: 372 """ 373 Verify a magic link token 374 Consumes a single-use token from a magic link URL and returns an access token, 375 refresh token, and the authenticated user object. This endpoint completes both the 376 login flow (initiated by `/api/v1/auth/login/link`) and the registration flow 377 (initiated by `/api/v1/auth/register/link` or `/api/v1/auth/request/link`). 378 Extract the token from the `token` query parameter of the magic link redirect URI 379 and POST JSON `{"token":"<token from redirect URI>"}` to 380 `/api/v1/auth/verify/link`, not `/api/v1/auth/token`. Expired or already-used tokens return HTTP 401 expired links 381 have `error.code` = `authentication_error` and `error.message` = 382 `expired_token`; unknown or already-used tokens have `error.message` = 383 `invalid_or_expired_token`. If the app has disabled passwordless authentication 384 the request returns HTTP 403. Rate-limited to 10 requests per IP per minute 385 exceeding this returns HTTP 429. 386 387 Args: 388 token: Single-use magic link token extracted from the redirect URI query parameter. 389 390 Returns: 391 Access token, refresh token, and the authenticated user object. 392 """ 393 body: dict[str, object] = {} 394 if token is not None: 395 body["token"] = token 396 397 data = await self._http.request( 398 "/api/v1/auth/verify/link", 399 method="POST", 400 body=body, 401 ) 402 return AuthTokens( 403 token_expiry=data.get("expires_in"), 404 refresh_token=data.get("refresh_token"), 405 access_token=data.get("token"), 406 )
24 async def allowed_auth_methods(self) -> dict: 25 """ 26 List supported auth methods 27 Returns the complete catalogue of authentication methods the platform supports, 28 including each method's stable slug, user-facing name, and description. 29 Use this endpoint to render method labels in sign-in UIs or org settings screens 30 without hardcoding copy or maintaining your own enum list. Results reflect the 31 platform's source-of-truth catalogue and are consistent across all orgs. 32 This endpoint requires only a publishable key and is accessible without an active 33 user session, making it suitable for pre-authentication flows such as login page 34 rendering or onboarding configuration. 35 36 Returns: 37 Successful response 38 """ 39 data = await self._http.request( 40 "/api/v1/auth/allowed_auth_methods", 41 method="GET", 42 ) 43 return data
List supported auth methods Returns the complete catalogue of authentication methods the platform supports, including each method's stable slug, user-facing name, and description. Use this endpoint to render method labels in sign-in UIs or org settings screens without hardcoding copy or maintaining your own enum list. Results reflect the platform's source-of-truth catalogue and are consistent across all orgs. This endpoint requires only a publishable key and is accessible without an active user session, making it suitable for pre-authentication flows such as login page rendering or onboarding configuration.
Returns:
Successful response
45 async def login(self, email: str, password: str) -> AuthTokens: 46 """ 47 Authenticate with email and password 48 Authenticates a user with an email address and password and returns a short-lived 49 access token, a refresh token, and the authenticated user object. Use the refresh 50 token with the `/auth/refresh` endpoint to obtain new access tokens without 51 re-authenticating. 52 Password login must be enabled for the app; apps that have disabled password 53 authentication return HTTP 403. Requests are rate-limited per IP (10 per minute) 54 and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429. 55 56 Args: 57 email: Email address of the user to authenticate. 58 password: Password for the account associated with the given email. 59 60 Returns: 61 Access token, refresh token, and authenticated user object. 62 """ 63 body: dict[str, object] = {} 64 body["email"] = email 65 body["password"] = password 66 67 data = await self._http.request( 68 "/api/v1/auth/login", 69 method="POST", 70 body=body, 71 ) 72 return AuthTokens( 73 token_expiry=data.get("expires_in"), 74 refresh_token=data.get("refresh_token"), 75 access_token=data.get("token"), 76 )
Authenticate with email and password
Authenticates a user with an email address and password and returns a short-lived
access token, a refresh token, and the authenticated user object. Use the refresh
token with the /auth/refresh endpoint to obtain new access tokens without
re-authenticating.
Password login must be enabled for the app; apps that have disabled password
authentication return HTTP 403. Requests are rate-limited per IP (10 per minute)
and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429.
Arguments:
- email: Email address of the user to authenticate.
- password: Password for the account associated with the given email.
Returns:
Access token, refresh token, and authenticated user object.
78 async def request_login_magic_link( 79 self, email: str | None = None, redirect_uri: str | None = None 80 ) -> dict: 81 """ 82 Request a magic link for login 83 Sends a magic link to the given email address so an existing user can sign in 84 without a password. The user clicks the link in their email and is redirected to 85 `redirect_uri` with a token; pass that token to `/api/v1/auth/verify/link` to obtain 86 session tokens. 87 If no account exists for the email, the endpoint still returns success to prevent 88 email enumeration no link is sent in that case. Both `email` and `redirect_uri` 89 are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair 90 (3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success. 91 92 Args: 93 email: Email address of the account to send the magic link to. 94 redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter. 95 96 Returns: 97 No content 98 """ 99 body: dict[str, object] = {} 100 if email is not None: 101 body["email"] = email 102 if redirect_uri is not None: 103 body["redirect_uri"] = redirect_uri 104 105 data = await self._http.request( 106 "/api/v1/auth/login/link", 107 method="POST", 108 body=body, 109 ) 110 return data
Request a magic link for login
Sends a magic link to the given email address so an existing user can sign in
without a password. The user clicks the link in their email and is redirected to
redirect_uri with a token; pass that token to /api/v1/auth/verify/link to obtain
session tokens.
If no account exists for the email, the endpoint still returns success to prevent
email enumeration no link is sent in that case. Both email and redirect_uri
are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair
(3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success.
Arguments:
- email: Email address of the account to send the magic link to.
- redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter.
Returns:
No content
112 async def refresh(self, refresh_token: str) -> AuthTokens: 113 """ 114 Refresh an access token 115 Exchanges a valid refresh token for a new access token and a new refresh token, 116 rotating the refresh token on every call. The response also includes the updated 117 user object. Store the new refresh token and discard the old one. 118 Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. 119 Rate limiting is applied per (user, IP) pair when the token can be verified, and 120 falls back to IP-only when it cannot. The limit is 30 exchanges per minute per 121 bucket; exceeding it returns HTTP 429. 122 123 Args: 124 refresh_token: Refresh token previously issued by a login, registration, or token-refresh response. 125 126 Returns: 127 New access token, new refresh token, and the authenticated user object. 128 """ 129 body: dict[str, object] = {} 130 body["refresh_token"] = refresh_token 131 132 data = await self._http.request( 133 "/api/v1/auth/refresh", 134 method="POST", 135 body=body, 136 ) 137 return AuthTokens( 138 token_expiry=data.get("expires_in"), 139 refresh_token=data.get("refresh_token"), 140 access_token=data.get("token"), 141 )
Refresh an access token Exchanges a valid refresh token for a new access token and a new refresh token, rotating the refresh token on every call. The response also includes the updated user object. Store the new refresh token and discard the old one. Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. Rate limiting is applied per (user, IP) pair when the token can be verified, and falls back to IP-only when it cannot. The limit is 30 exchanges per minute per bucket; exceeding it returns HTTP 429.
Arguments:
- refresh_token: Refresh token previously issued by a login, registration, or token-refresh response.
Returns:
New access token, new refresh token, and the authenticated user object.
143 async def register( 144 self, 145 email: str, 146 alias: str | None = None, 147 full_name: str | None = None, 148 invite_code: str | None = None, 149 password: str | None = None, 150 set_org: str | None = None, 151 team_invite: str | None = None, 152 timezone: str | None = None, 153 ) -> AuthTokens: 154 """ 155 Register a new user with email and password 156 Creates a new user account and returns an access token, refresh token, and the new 157 user object. Two registration paths are supported: 158 - **Team registration**: supply `team_invite` with a valid team invite ID. The new 159 user is added to that team immediately upon registration. Returns HTTP 404 if the 160 invite is not found. 161 - **Standard registration**: supply `password`. An `invite_code` may optionally be 162 included for invite-gated apps; an invalid code returns HTTP 404. 163 Provide `team_invite` or `password`; when both are supplied, team registration 164 takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 165 with field-level guidance. Use `full_name` for the profile name (returned as 166 `user.name`); `name` is not an input field. `alias` is a separate optional handle. 167 Legacy `team_invite_id` remains accepted; `team_invite` takes precedence. 168 Password registration must be enabled for the app; disabled apps return 169 HTTP 403. The response status is HTTP 201 on success. 170 171 Args: 172 email: Email address for the new account. 173 alias: Display alias (handle) for the new account. 174 full_name: Full name for the new account. 175 invite_code: Invite code for invite-gated registration. Applied only in the standard registration path. 176 password: Password for the new account. Required for standard (non-team-invite) registration. 177 set_org: Create or reuse an organization from the work-email domain and stamp the new user into it. 178 team_invite: Team invite ID. When provided, the user is added to the team on registration. 179 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 180 181 Returns: 182 Access token, refresh token, and the newly created user object. 183 """ 184 body: dict[str, object] = {} 185 body["email"] = email 186 if alias is not None: 187 body["alias"] = alias 188 if full_name is not None: 189 body["full_name"] = full_name 190 if invite_code is not None: 191 body["invite_code"] = invite_code 192 if password is not None: 193 body["password"] = password 194 if set_org is not None: 195 body["set_org"] = set_org 196 if team_invite is not None: 197 body["team_invite"] = team_invite 198 if timezone is not None: 199 body["timezone"] = timezone 200 201 data = await self._http.request( 202 "/api/v1/auth/register", 203 method="POST", 204 body=body, 205 ) 206 return AuthTokens( 207 token_expiry=data.get("expires_in"), 208 refresh_token=data.get("refresh_token"), 209 access_token=data.get("token"), 210 )
Register a new user with email and password Creates a new user account and returns an access token, refresh token, and the new user object. Two registration paths are supported:
- Team registration: supply
team_invitewith a valid team invite ID. The new user is added to that team immediately upon registration. Returns HTTP 404 if the invite is not found. - Standard registration: supply
password. Aninvite_codemay optionally be included for invite-gated apps; an invalid code returns HTTP 404. Provideteam_inviteorpassword; when both are supplied, team registration takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 with field-level guidance. Usefull_namefor the profile name (returned asuser.name);nameis not an input field.aliasis a separate optional handle. Legacyteam_invite_idremains accepted;team_invitetakes precedence. Password registration must be enabled for the app; disabled apps return HTTP 403. The response status is HTTP 201 on success.
Arguments:
- email: Email address for the new account.
- alias: Display alias (handle) for the new account.
- full_name: Full name for the new account.
- invite_code: Invite code for invite-gated registration. Applied only in the standard registration path.
- password: Password for the new account. Required for standard (non-team-invite) registration.
- set_org: Create or reuse an organization from the work-email domain and stamp the new user into it.
- team_invite: Team invite ID. When provided, the user is added to the team on registration.
- timezone: IANA timezone name for the new account, e.g.
"America/New_York".
Returns:
Access token, refresh token, and the newly created user object.
212 async def request_register_magic_link( 213 self, 214 alias: str | None = None, 215 email: str | None = None, 216 full_name: str | None = None, 217 org: str | None = None, 218 redirect_uri: str | None = None, 219 set_org: str | None = None, 220 timezone: str | None = None, 221 ) -> dict: 222 """ 223 Request a magic link for registration 224 Starts a passwordless registration flow by sending a verification link to the given 225 email address. The recipient clicks the link and is redirected to `redirect_uri` with 226 a token; pass that token to `/api/v1/auth/verify/link` to complete registration and obtain 227 session tokens. 228 Profile fields (`full_name`, `alias`, `timezone`) and, with `set_org`, the `org` 229 details are captured now and applied when the link is verified; nothing is created 230 before the click. Requests are rate-limited per IP (10 per minute) and per 231 email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns 232 HTTP 204 on success. 233 234 Args: 235 alias: Display alias (handle) for the new account. 236 email: Email address to send the registration magic link to. 237 full_name: Full name for the new account. 238 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 239 redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter. 240 set_org: Create or reuse an organization from the work-email domain during confirmation. 241 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 242 243 Returns: 244 No content 245 """ 246 body: dict[str, object] = {} 247 if alias is not None: 248 body["alias"] = alias 249 if email is not None: 250 body["email"] = email 251 if full_name is not None: 252 body["full_name"] = full_name 253 if org is not None: 254 body["org"] = org 255 if redirect_uri is not None: 256 body["redirect_uri"] = redirect_uri 257 if set_org is not None: 258 body["set_org"] = set_org 259 if timezone is not None: 260 body["timezone"] = timezone 261 262 data = await self._http.request( 263 "/api/v1/auth/register/link", 264 method="POST", 265 body=body, 266 ) 267 return data
Request a magic link for registration
Starts a passwordless registration flow by sending a verification link to the given
email address. The recipient clicks the link and is redirected to redirect_uri with
a token; pass that token to /api/v1/auth/verify/link to complete registration and obtain
session tokens.
Profile fields (full_name, alias, timezone) and, with set_org, the org
details are captured now and applied when the link is verified; nothing is created
before the click. Requests are rate-limited per IP (10 per minute) and per
email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns
HTTP 204 on success.
Arguments:
- alias: Display alias (handle) for the new account.
- email: Email address to send the registration magic link to.
- full_name: Full name for the new account.
- org: Company details for the organization a
set_orgregistration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). - redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter.
- set_org: Create or reuse an organization from the work-email domain during confirmation.
- timezone: IANA timezone name for the new account, e.g.
"America/New_York".
Returns:
No content
269 async def request_magic_link( 270 self, 271 email: str | None = None, 272 full_name: str | None = None, 273 mcp_client_id: str | None = None, 274 org: str | None = None, 275 redirect_uri: str | None = None, 276 set_org: str | None = None, 277 signup_origin: str | None = None, 278 ) -> dict: 279 """ 280 Request a magic link for login or registration 281 Sends a passwordless magic link to the given email address. If an account with that 282 email already exists, a login link is sent. If no account exists, a registration link 283 is sent and the recipient completes sign-up by clicking through. This unified endpoint 284 lets you implement a single email-entry UI that handles both cases transparently. 285 The `redirect_uri` is validated against the app's registered hosts; an unregistered 286 URI returns HTTP 400. Both `email` and `redirect_uri` are required. Requests are 287 rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns 288 HTTP 204 on success no body. If the app has Allowed Users enabled and the 289 address is not on the list, returns HTTP 403 `user_not_allowed`. That refusal 290 is the same for new and existing addresses, so it does not reveal whether an 291 account exists. 292 For a new user, `full_name` and (with `set_org`) the `org` details ride the pending 293 registration and are applied when the link is verified; nothing is created before 294 the click. They are ignored when the email already belongs to a user. Malformed 295 values return HTTP 422 before the address is looked up, so the response cannot 296 reveal whether an account exists. 297 298 Args: 299 email: Email address to send the magic link to. 300 full_name: Display name for the account the verified link will create. 301 mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key. 302 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 303 redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app. 304 set_org: For a new user, create or reuse an organization from the work-email domain during confirmation. 305 signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication. 306 307 Returns: 308 No content 309 """ 310 body: dict[str, object] = {} 311 if email is not None: 312 body["email"] = email 313 if full_name is not None: 314 body["full_name"] = full_name 315 if mcp_client_id is not None: 316 body["mcp_client_id"] = mcp_client_id 317 if org is not None: 318 body["org"] = org 319 if redirect_uri is not None: 320 body["redirect_uri"] = redirect_uri 321 if set_org is not None: 322 body["set_org"] = set_org 323 if signup_origin is not None: 324 body["signup_origin"] = signup_origin 325 326 data = await self._http.request( 327 "/api/v1/auth/request/link", 328 method="POST", 329 body=body, 330 ) 331 return data
Request a magic link for login or registration
Sends a passwordless magic link to the given email address. If an account with that
email already exists, a login link is sent. If no account exists, a registration link
is sent and the recipient completes sign-up by clicking through. This unified endpoint
lets you implement a single email-entry UI that handles both cases transparently.
The redirect_uri is validated against the app's registered hosts; an unregistered
URI returns HTTP 400. Both email and redirect_uri are required. Requests are
rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns
HTTP 204 on success no body. If the app has Allowed Users enabled and the
address is not on the list, returns HTTP 403 user_not_allowed. That refusal
is the same for new and existing addresses, so it does not reveal whether an
account exists.
For a new user, full_name and (with set_org) the org details ride the pending
registration and are applied when the link is verified; nothing is created before
the click. They are ignored when the email already belongs to a user. Malformed
values return HTTP 422 before the address is looked up, so the response cannot
reveal whether an account exists.
Arguments:
- email: Email address to send the magic link to.
- full_name: Display name for the account the verified link will create.
- mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key.
- org: Company details for the organization a
set_orgregistration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). - redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app.
- set_org: For a new user, create or reuse an organization from the work-email domain during confirmation.
- signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication.
Returns:
No content
333 async def exchange_login_token(self, token: str, timezone: str | None = None) -> AuthTokens: 334 """ 335 Exchange a one-time login token for session tokens 336 Exchanges an email-login token (including email reply links) or an internal 337 impersonation login token for session credentials. Passwordless magic-link tokens 338 from `/api/v1/auth/login/link`, `/api/v1/auth/register/link`, or 339 `/api/v1/auth/request/link` must instead go to `/api/v1/auth/verify/link`. 340 Numeric email codes go to `/api/v1/auth/code/verify` with `email` and `code`. 341 Access and refresh tokens are not accepted here. Invalid, expired, or already-used 342 login tokens return HTTP 401 with structured `error.type`, `error.code`, and 343 `error.message` fields. 344 If `timezone` is provided and the user's current timezone is still the default 345 (`"America/Los_Angeles"`), the account timezone is updated in the same request. 346 Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429. 347 348 Args: 349 token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code. 350 timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g. `"Europe/London"`. Omit to leave the timezone unchanged. 351 352 Returns: 353 Access token, refresh token, and the authenticated user object. 354 """ 355 body: dict[str, object] = {} 356 body["token"] = token 357 if timezone is not None: 358 body["timezone"] = timezone 359 360 data = await self._http.request( 361 "/api/v1/auth/token", 362 method="POST", 363 body=body, 364 ) 365 return AuthTokens( 366 token_expiry=data.get("expires_in"), 367 refresh_token=data.get("refresh_token"), 368 access_token=data.get("token"), 369 )
Exchange a one-time login token for session tokens
Exchanges an email-login token (including email reply links) or an internal
impersonation login token for session credentials. Passwordless magic-link tokens
from /api/v1/auth/login/link, /api/v1/auth/register/link, or
/api/v1/auth/request/link must instead go to /api/v1/auth/verify/link.
Numeric email codes go to /api/v1/auth/code/verify with email and code.
Access and refresh tokens are not accepted here. Invalid, expired, or already-used
login tokens return HTTP 401 with structured error.type, error.code, and
error.message fields.
If timezone is provided and the user's current timezone is still the default
("America/Los_Angeles"), the account timezone is updated in the same request.
Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429.
Arguments:
- token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code.
- timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g.
"Europe/London". Omit to leave the timezone unchanged.
Returns:
Access token, refresh token, and the authenticated user object.
371 async def verify_magic_link(self, token: str | None = None) -> AuthTokens: 372 """ 373 Verify a magic link token 374 Consumes a single-use token from a magic link URL and returns an access token, 375 refresh token, and the authenticated user object. This endpoint completes both the 376 login flow (initiated by `/api/v1/auth/login/link`) and the registration flow 377 (initiated by `/api/v1/auth/register/link` or `/api/v1/auth/request/link`). 378 Extract the token from the `token` query parameter of the magic link redirect URI 379 and POST JSON `{"token":"<token from redirect URI>"}` to 380 `/api/v1/auth/verify/link`, not `/api/v1/auth/token`. Expired or already-used tokens return HTTP 401 expired links 381 have `error.code` = `authentication_error` and `error.message` = 382 `expired_token`; unknown or already-used tokens have `error.message` = 383 `invalid_or_expired_token`. If the app has disabled passwordless authentication 384 the request returns HTTP 403. Rate-limited to 10 requests per IP per minute 385 exceeding this returns HTTP 429. 386 387 Args: 388 token: Single-use magic link token extracted from the redirect URI query parameter. 389 390 Returns: 391 Access token, refresh token, and the authenticated user object. 392 """ 393 body: dict[str, object] = {} 394 if token is not None: 395 body["token"] = token 396 397 data = await self._http.request( 398 "/api/v1/auth/verify/link", 399 method="POST", 400 body=body, 401 ) 402 return AuthTokens( 403 token_expiry=data.get("expires_in"), 404 refresh_token=data.get("refresh_token"), 405 access_token=data.get("token"), 406 )
Verify a magic link token
Consumes a single-use token from a magic link URL and returns an access token,
refresh token, and the authenticated user object. This endpoint completes both the
login flow (initiated by /api/v1/auth/login/link) and the registration flow
(initiated by /api/v1/auth/register/link or /api/v1/auth/request/link).
Extract the token from the token query parameter of the magic link redirect URI
and POST JSON {"token":"<token from redirect URI>"} to
/api/v1/auth/verify/link, not /api/v1/auth/token. Expired or already-used tokens return HTTP 401 expired links
have error.code = authentication_error and error.message =
expired_token; unknown or already-used tokens have error.message =
invalid_or_expired_token. If the app has disabled passwordless authentication
the request returns HTTP 403. Rate-limited to 10 requests per IP per minute
exceeding this returns HTTP 429.
Arguments:
- token: Single-use magic link token extracted from the redirect URI query parameter.
Returns:
Access token, refresh token, and the authenticated user object.
409class AuthClient: 410 def __init__(self, http: SyncHttpClient): 411 self._http = http 412 413 def allowed_auth_methods(self) -> dict: 414 """ 415 List supported auth methods 416 Returns the complete catalogue of authentication methods the platform supports, 417 including each method's stable slug, user-facing name, and description. 418 Use this endpoint to render method labels in sign-in UIs or org settings screens 419 without hardcoding copy or maintaining your own enum list. Results reflect the 420 platform's source-of-truth catalogue and are consistent across all orgs. 421 This endpoint requires only a publishable key and is accessible without an active 422 user session, making it suitable for pre-authentication flows such as login page 423 rendering or onboarding configuration. 424 425 Returns: 426 Successful response 427 """ 428 data = self._http.request( 429 "/api/v1/auth/allowed_auth_methods", 430 method="GET", 431 ) 432 return data 433 434 def login(self, email: str, password: str) -> AuthTokens: 435 """ 436 Authenticate with email and password 437 Authenticates a user with an email address and password and returns a short-lived 438 access token, a refresh token, and the authenticated user object. Use the refresh 439 token with the `/auth/refresh` endpoint to obtain new access tokens without 440 re-authenticating. 441 Password login must be enabled for the app; apps that have disabled password 442 authentication return HTTP 403. Requests are rate-limited per IP (10 per minute) 443 and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429. 444 445 Args: 446 email: Email address of the user to authenticate. 447 password: Password for the account associated with the given email. 448 449 Returns: 450 Access token, refresh token, and authenticated user object. 451 """ 452 body: dict[str, object] = {} 453 body["email"] = email 454 body["password"] = password 455 456 data = self._http.request( 457 "/api/v1/auth/login", 458 method="POST", 459 body=body, 460 ) 461 return AuthTokens( 462 token_expiry=data.get("expires_in"), 463 refresh_token=data.get("refresh_token"), 464 access_token=data.get("token"), 465 ) 466 467 def request_login_magic_link( 468 self, email: str | None = None, redirect_uri: str | None = None 469 ) -> dict: 470 """ 471 Request a magic link for login 472 Sends a magic link to the given email address so an existing user can sign in 473 without a password. The user clicks the link in their email and is redirected to 474 `redirect_uri` with a token; pass that token to `/api/v1/auth/verify/link` to obtain 475 session tokens. 476 If no account exists for the email, the endpoint still returns success to prevent 477 email enumeration no link is sent in that case. Both `email` and `redirect_uri` 478 are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair 479 (3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success. 480 481 Args: 482 email: Email address of the account to send the magic link to. 483 redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter. 484 485 Returns: 486 No content 487 """ 488 body: dict[str, object] = {} 489 if email is not None: 490 body["email"] = email 491 if redirect_uri is not None: 492 body["redirect_uri"] = redirect_uri 493 494 data = self._http.request( 495 "/api/v1/auth/login/link", 496 method="POST", 497 body=body, 498 ) 499 return data 500 501 def refresh(self, refresh_token: str) -> AuthTokens: 502 """ 503 Refresh an access token 504 Exchanges a valid refresh token for a new access token and a new refresh token, 505 rotating the refresh token on every call. The response also includes the updated 506 user object. Store the new refresh token and discard the old one. 507 Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. 508 Rate limiting is applied per (user, IP) pair when the token can be verified, and 509 falls back to IP-only when it cannot. The limit is 30 exchanges per minute per 510 bucket; exceeding it returns HTTP 429. 511 512 Args: 513 refresh_token: Refresh token previously issued by a login, registration, or token-refresh response. 514 515 Returns: 516 New access token, new refresh token, and the authenticated user object. 517 """ 518 body: dict[str, object] = {} 519 body["refresh_token"] = refresh_token 520 521 data = self._http.request( 522 "/api/v1/auth/refresh", 523 method="POST", 524 body=body, 525 ) 526 return AuthTokens( 527 token_expiry=data.get("expires_in"), 528 refresh_token=data.get("refresh_token"), 529 access_token=data.get("token"), 530 ) 531 532 def register( 533 self, 534 email: str, 535 alias: str | None = None, 536 full_name: str | None = None, 537 invite_code: str | None = None, 538 password: str | None = None, 539 set_org: str | None = None, 540 team_invite: str | None = None, 541 timezone: str | None = None, 542 ) -> AuthTokens: 543 """ 544 Register a new user with email and password 545 Creates a new user account and returns an access token, refresh token, and the new 546 user object. Two registration paths are supported: 547 - **Team registration**: supply `team_invite` with a valid team invite ID. The new 548 user is added to that team immediately upon registration. Returns HTTP 404 if the 549 invite is not found. 550 - **Standard registration**: supply `password`. An `invite_code` may optionally be 551 included for invite-gated apps; an invalid code returns HTTP 404. 552 Provide `team_invite` or `password`; when both are supplied, team registration 553 takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 554 with field-level guidance. Use `full_name` for the profile name (returned as 555 `user.name`); `name` is not an input field. `alias` is a separate optional handle. 556 Legacy `team_invite_id` remains accepted; `team_invite` takes precedence. 557 Password registration must be enabled for the app; disabled apps return 558 HTTP 403. The response status is HTTP 201 on success. 559 560 Args: 561 email: Email address for the new account. 562 alias: Display alias (handle) for the new account. 563 full_name: Full name for the new account. 564 invite_code: Invite code for invite-gated registration. Applied only in the standard registration path. 565 password: Password for the new account. Required for standard (non-team-invite) registration. 566 set_org: Create or reuse an organization from the work-email domain and stamp the new user into it. 567 team_invite: Team invite ID. When provided, the user is added to the team on registration. 568 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 569 570 Returns: 571 Access token, refresh token, and the newly created user object. 572 """ 573 body: dict[str, object] = {} 574 body["email"] = email 575 if alias is not None: 576 body["alias"] = alias 577 if full_name is not None: 578 body["full_name"] = full_name 579 if invite_code is not None: 580 body["invite_code"] = invite_code 581 if password is not None: 582 body["password"] = password 583 if set_org is not None: 584 body["set_org"] = set_org 585 if team_invite is not None: 586 body["team_invite"] = team_invite 587 if timezone is not None: 588 body["timezone"] = timezone 589 590 data = self._http.request( 591 "/api/v1/auth/register", 592 method="POST", 593 body=body, 594 ) 595 return AuthTokens( 596 token_expiry=data.get("expires_in"), 597 refresh_token=data.get("refresh_token"), 598 access_token=data.get("token"), 599 ) 600 601 def request_register_magic_link( 602 self, 603 alias: str | None = None, 604 email: str | None = None, 605 full_name: str | None = None, 606 org: str | None = None, 607 redirect_uri: str | None = None, 608 set_org: str | None = None, 609 timezone: str | None = None, 610 ) -> dict: 611 """ 612 Request a magic link for registration 613 Starts a passwordless registration flow by sending a verification link to the given 614 email address. The recipient clicks the link and is redirected to `redirect_uri` with 615 a token; pass that token to `/api/v1/auth/verify/link` to complete registration and obtain 616 session tokens. 617 Profile fields (`full_name`, `alias`, `timezone`) and, with `set_org`, the `org` 618 details are captured now and applied when the link is verified; nothing is created 619 before the click. Requests are rate-limited per IP (10 per minute) and per 620 email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns 621 HTTP 204 on success. 622 623 Args: 624 alias: Display alias (handle) for the new account. 625 email: Email address to send the registration magic link to. 626 full_name: Full name for the new account. 627 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 628 redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter. 629 set_org: Create or reuse an organization from the work-email domain during confirmation. 630 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 631 632 Returns: 633 No content 634 """ 635 body: dict[str, object] = {} 636 if alias is not None: 637 body["alias"] = alias 638 if email is not None: 639 body["email"] = email 640 if full_name is not None: 641 body["full_name"] = full_name 642 if org is not None: 643 body["org"] = org 644 if redirect_uri is not None: 645 body["redirect_uri"] = redirect_uri 646 if set_org is not None: 647 body["set_org"] = set_org 648 if timezone is not None: 649 body["timezone"] = timezone 650 651 data = self._http.request( 652 "/api/v1/auth/register/link", 653 method="POST", 654 body=body, 655 ) 656 return data 657 658 def request_magic_link( 659 self, 660 email: str | None = None, 661 full_name: str | None = None, 662 mcp_client_id: str | None = None, 663 org: str | None = None, 664 redirect_uri: str | None = None, 665 set_org: str | None = None, 666 signup_origin: str | None = None, 667 ) -> dict: 668 """ 669 Request a magic link for login or registration 670 Sends a passwordless magic link to the given email address. If an account with that 671 email already exists, a login link is sent. If no account exists, a registration link 672 is sent and the recipient completes sign-up by clicking through. This unified endpoint 673 lets you implement a single email-entry UI that handles both cases transparently. 674 The `redirect_uri` is validated against the app's registered hosts; an unregistered 675 URI returns HTTP 400. Both `email` and `redirect_uri` are required. Requests are 676 rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns 677 HTTP 204 on success no body. If the app has Allowed Users enabled and the 678 address is not on the list, returns HTTP 403 `user_not_allowed`. That refusal 679 is the same for new and existing addresses, so it does not reveal whether an 680 account exists. 681 For a new user, `full_name` and (with `set_org`) the `org` details ride the pending 682 registration and are applied when the link is verified; nothing is created before 683 the click. They are ignored when the email already belongs to a user. Malformed 684 values return HTTP 422 before the address is looked up, so the response cannot 685 reveal whether an account exists. 686 687 Args: 688 email: Email address to send the magic link to. 689 full_name: Display name for the account the verified link will create. 690 mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key. 691 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 692 redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app. 693 set_org: For a new user, create or reuse an organization from the work-email domain during confirmation. 694 signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication. 695 696 Returns: 697 No content 698 """ 699 body: dict[str, object] = {} 700 if email is not None: 701 body["email"] = email 702 if full_name is not None: 703 body["full_name"] = full_name 704 if mcp_client_id is not None: 705 body["mcp_client_id"] = mcp_client_id 706 if org is not None: 707 body["org"] = org 708 if redirect_uri is not None: 709 body["redirect_uri"] = redirect_uri 710 if set_org is not None: 711 body["set_org"] = set_org 712 if signup_origin is not None: 713 body["signup_origin"] = signup_origin 714 715 data = self._http.request( 716 "/api/v1/auth/request/link", 717 method="POST", 718 body=body, 719 ) 720 return data 721 722 def exchange_login_token(self, token: str, timezone: str | None = None) -> AuthTokens: 723 """ 724 Exchange a one-time login token for session tokens 725 Exchanges an email-login token (including email reply links) or an internal 726 impersonation login token for session credentials. Passwordless magic-link tokens 727 from `/api/v1/auth/login/link`, `/api/v1/auth/register/link`, or 728 `/api/v1/auth/request/link` must instead go to `/api/v1/auth/verify/link`. 729 Numeric email codes go to `/api/v1/auth/code/verify` with `email` and `code`. 730 Access and refresh tokens are not accepted here. Invalid, expired, or already-used 731 login tokens return HTTP 401 with structured `error.type`, `error.code`, and 732 `error.message` fields. 733 If `timezone` is provided and the user's current timezone is still the default 734 (`"America/Los_Angeles"`), the account timezone is updated in the same request. 735 Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429. 736 737 Args: 738 token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code. 739 timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g. `"Europe/London"`. Omit to leave the timezone unchanged. 740 741 Returns: 742 Access token, refresh token, and the authenticated user object. 743 """ 744 body: dict[str, object] = {} 745 body["token"] = token 746 if timezone is not None: 747 body["timezone"] = timezone 748 749 data = self._http.request( 750 "/api/v1/auth/token", 751 method="POST", 752 body=body, 753 ) 754 return AuthTokens( 755 token_expiry=data.get("expires_in"), 756 refresh_token=data.get("refresh_token"), 757 access_token=data.get("token"), 758 ) 759 760 def verify_magic_link(self, token: str | None = None) -> AuthTokens: 761 """ 762 Verify a magic link token 763 Consumes a single-use token from a magic link URL and returns an access token, 764 refresh token, and the authenticated user object. This endpoint completes both the 765 login flow (initiated by `/api/v1/auth/login/link`) and the registration flow 766 (initiated by `/api/v1/auth/register/link` or `/api/v1/auth/request/link`). 767 Extract the token from the `token` query parameter of the magic link redirect URI 768 and POST JSON `{"token":"<token from redirect URI>"}` to 769 `/api/v1/auth/verify/link`, not `/api/v1/auth/token`. Expired or already-used tokens return HTTP 401 expired links 770 have `error.code` = `authentication_error` and `error.message` = 771 `expired_token`; unknown or already-used tokens have `error.message` = 772 `invalid_or_expired_token`. If the app has disabled passwordless authentication 773 the request returns HTTP 403. Rate-limited to 10 requests per IP per minute 774 exceeding this returns HTTP 429. 775 776 Args: 777 token: Single-use magic link token extracted from the redirect URI query parameter. 778 779 Returns: 780 Access token, refresh token, and the authenticated user object. 781 """ 782 body: dict[str, object] = {} 783 if token is not None: 784 body["token"] = token 785 786 data = self._http.request( 787 "/api/v1/auth/verify/link", 788 method="POST", 789 body=body, 790 ) 791 return AuthTokens( 792 token_expiry=data.get("expires_in"), 793 refresh_token=data.get("refresh_token"), 794 access_token=data.get("token"), 795 )
413 def allowed_auth_methods(self) -> dict: 414 """ 415 List supported auth methods 416 Returns the complete catalogue of authentication methods the platform supports, 417 including each method's stable slug, user-facing name, and description. 418 Use this endpoint to render method labels in sign-in UIs or org settings screens 419 without hardcoding copy or maintaining your own enum list. Results reflect the 420 platform's source-of-truth catalogue and are consistent across all orgs. 421 This endpoint requires only a publishable key and is accessible without an active 422 user session, making it suitable for pre-authentication flows such as login page 423 rendering or onboarding configuration. 424 425 Returns: 426 Successful response 427 """ 428 data = self._http.request( 429 "/api/v1/auth/allowed_auth_methods", 430 method="GET", 431 ) 432 return data
List supported auth methods Returns the complete catalogue of authentication methods the platform supports, including each method's stable slug, user-facing name, and description. Use this endpoint to render method labels in sign-in UIs or org settings screens without hardcoding copy or maintaining your own enum list. Results reflect the platform's source-of-truth catalogue and are consistent across all orgs. This endpoint requires only a publishable key and is accessible without an active user session, making it suitable for pre-authentication flows such as login page rendering or onboarding configuration.
Returns:
Successful response
434 def login(self, email: str, password: str) -> AuthTokens: 435 """ 436 Authenticate with email and password 437 Authenticates a user with an email address and password and returns a short-lived 438 access token, a refresh token, and the authenticated user object. Use the refresh 439 token with the `/auth/refresh` endpoint to obtain new access tokens without 440 re-authenticating. 441 Password login must be enabled for the app; apps that have disabled password 442 authentication return HTTP 403. Requests are rate-limited per IP (10 per minute) 443 and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429. 444 445 Args: 446 email: Email address of the user to authenticate. 447 password: Password for the account associated with the given email. 448 449 Returns: 450 Access token, refresh token, and authenticated user object. 451 """ 452 body: dict[str, object] = {} 453 body["email"] = email 454 body["password"] = password 455 456 data = self._http.request( 457 "/api/v1/auth/login", 458 method="POST", 459 body=body, 460 ) 461 return AuthTokens( 462 token_expiry=data.get("expires_in"), 463 refresh_token=data.get("refresh_token"), 464 access_token=data.get("token"), 465 )
Authenticate with email and password
Authenticates a user with an email address and password and returns a short-lived
access token, a refresh token, and the authenticated user object. Use the refresh
token with the /auth/refresh endpoint to obtain new access tokens without
re-authenticating.
Password login must be enabled for the app; apps that have disabled password
authentication return HTTP 403. Requests are rate-limited per IP (10 per minute)
and per email-IP pair (5 per minute) exceeding either limit returns HTTP 429.
Arguments:
- email: Email address of the user to authenticate.
- password: Password for the account associated with the given email.
Returns:
Access token, refresh token, and authenticated user object.
467 def request_login_magic_link( 468 self, email: str | None = None, redirect_uri: str | None = None 469 ) -> dict: 470 """ 471 Request a magic link for login 472 Sends a magic link to the given email address so an existing user can sign in 473 without a password. The user clicks the link in their email and is redirected to 474 `redirect_uri` with a token; pass that token to `/api/v1/auth/verify/link` to obtain 475 session tokens. 476 If no account exists for the email, the endpoint still returns success to prevent 477 email enumeration no link is sent in that case. Both `email` and `redirect_uri` 478 are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair 479 (3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success. 480 481 Args: 482 email: Email address of the account to send the magic link to. 483 redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter. 484 485 Returns: 486 No content 487 """ 488 body: dict[str, object] = {} 489 if email is not None: 490 body["email"] = email 491 if redirect_uri is not None: 492 body["redirect_uri"] = redirect_uri 493 494 data = self._http.request( 495 "/api/v1/auth/login/link", 496 method="POST", 497 body=body, 498 ) 499 return data
Request a magic link for login
Sends a magic link to the given email address so an existing user can sign in
without a password. The user clicks the link in their email and is redirected to
redirect_uri with a token; pass that token to /api/v1/auth/verify/link to obtain
session tokens.
If no account exists for the email, the endpoint still returns success to prevent
email enumeration no link is sent in that case. Both email and redirect_uri
are required. Requests are rate-limited per IP (10 per minute) and per email-IP pair
(3 per minute) exceeding either limit returns HTTP 429. Returns HTTP 204 on success.
Arguments:
- email: Email address of the account to send the magic link to.
- redirect_uri: URL the user is redirected to after clicking the magic link. The token is appended as a query parameter.
Returns:
No content
501 def refresh(self, refresh_token: str) -> AuthTokens: 502 """ 503 Refresh an access token 504 Exchanges a valid refresh token for a new access token and a new refresh token, 505 rotating the refresh token on every call. The response also includes the updated 506 user object. Store the new refresh token and discard the old one. 507 Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. 508 Rate limiting is applied per (user, IP) pair when the token can be verified, and 509 falls back to IP-only when it cannot. The limit is 30 exchanges per minute per 510 bucket; exceeding it returns HTTP 429. 511 512 Args: 513 refresh_token: Refresh token previously issued by a login, registration, or token-refresh response. 514 515 Returns: 516 New access token, new refresh token, and the authenticated user object. 517 """ 518 body: dict[str, object] = {} 519 body["refresh_token"] = refresh_token 520 521 data = self._http.request( 522 "/api/v1/auth/refresh", 523 method="POST", 524 body=body, 525 ) 526 return AuthTokens( 527 token_expiry=data.get("expires_in"), 528 refresh_token=data.get("refresh_token"), 529 access_token=data.get("token"), 530 )
Refresh an access token Exchanges a valid refresh token for a new access token and a new refresh token, rotating the refresh token on every call. The response also includes the updated user object. Store the new refresh token and discard the old one. Refresh tokens are single-use submitting an already-consumed token returns HTTP 401. Rate limiting is applied per (user, IP) pair when the token can be verified, and falls back to IP-only when it cannot. The limit is 30 exchanges per minute per bucket; exceeding it returns HTTP 429.
Arguments:
- refresh_token: Refresh token previously issued by a login, registration, or token-refresh response.
Returns:
New access token, new refresh token, and the authenticated user object.
532 def register( 533 self, 534 email: str, 535 alias: str | None = None, 536 full_name: str | None = None, 537 invite_code: str | None = None, 538 password: str | None = None, 539 set_org: str | None = None, 540 team_invite: str | None = None, 541 timezone: str | None = None, 542 ) -> AuthTokens: 543 """ 544 Register a new user with email and password 545 Creates a new user account and returns an access token, refresh token, and the new 546 user object. Two registration paths are supported: 547 - **Team registration**: supply `team_invite` with a valid team invite ID. The new 548 user is added to that team immediately upon registration. Returns HTTP 404 if the 549 invite is not found. 550 - **Standard registration**: supply `password`. An `invite_code` may optionally be 551 included for invite-gated apps; an invalid code returns HTTP 404. 552 Provide `team_invite` or `password`; when both are supplied, team registration 553 takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 554 with field-level guidance. Use `full_name` for the profile name (returned as 555 `user.name`); `name` is not an input field. `alias` is a separate optional handle. 556 Legacy `team_invite_id` remains accepted; `team_invite` takes precedence. 557 Password registration must be enabled for the app; disabled apps return 558 HTTP 403. The response status is HTTP 201 on success. 559 560 Args: 561 email: Email address for the new account. 562 alias: Display alias (handle) for the new account. 563 full_name: Full name for the new account. 564 invite_code: Invite code for invite-gated registration. Applied only in the standard registration path. 565 password: Password for the new account. Required for standard (non-team-invite) registration. 566 set_org: Create or reuse an organization from the work-email domain and stamp the new user into it. 567 team_invite: Team invite ID. When provided, the user is added to the team on registration. 568 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 569 570 Returns: 571 Access token, refresh token, and the newly created user object. 572 """ 573 body: dict[str, object] = {} 574 body["email"] = email 575 if alias is not None: 576 body["alias"] = alias 577 if full_name is not None: 578 body["full_name"] = full_name 579 if invite_code is not None: 580 body["invite_code"] = invite_code 581 if password is not None: 582 body["password"] = password 583 if set_org is not None: 584 body["set_org"] = set_org 585 if team_invite is not None: 586 body["team_invite"] = team_invite 587 if timezone is not None: 588 body["timezone"] = timezone 589 590 data = self._http.request( 591 "/api/v1/auth/register", 592 method="POST", 593 body=body, 594 ) 595 return AuthTokens( 596 token_expiry=data.get("expires_in"), 597 refresh_token=data.get("refresh_token"), 598 access_token=data.get("token"), 599 )
Register a new user with email and password Creates a new user account and returns an access token, refresh token, and the new user object. Two registration paths are supported:
- Team registration: supply
team_invitewith a valid team invite ID. The new user is added to that team immediately upon registration. Returns HTTP 404 if the invite is not found. - Standard registration: supply
password. Aninvite_codemay optionally be included for invite-gated apps; an invalid code returns HTTP 404. Provideteam_inviteorpassword; when both are supplied, team registration takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 with field-level guidance. Usefull_namefor the profile name (returned asuser.name);nameis not an input field.aliasis a separate optional handle. Legacyteam_invite_idremains accepted;team_invitetakes precedence. Password registration must be enabled for the app; disabled apps return HTTP 403. The response status is HTTP 201 on success.
Arguments:
- email: Email address for the new account.
- alias: Display alias (handle) for the new account.
- full_name: Full name for the new account.
- invite_code: Invite code for invite-gated registration. Applied only in the standard registration path.
- password: Password for the new account. Required for standard (non-team-invite) registration.
- set_org: Create or reuse an organization from the work-email domain and stamp the new user into it.
- team_invite: Team invite ID. When provided, the user is added to the team on registration.
- timezone: IANA timezone name for the new account, e.g.
"America/New_York".
Returns:
Access token, refresh token, and the newly created user object.
601 def request_register_magic_link( 602 self, 603 alias: str | None = None, 604 email: str | None = None, 605 full_name: str | None = None, 606 org: str | None = None, 607 redirect_uri: str | None = None, 608 set_org: str | None = None, 609 timezone: str | None = None, 610 ) -> dict: 611 """ 612 Request a magic link for registration 613 Starts a passwordless registration flow by sending a verification link to the given 614 email address. The recipient clicks the link and is redirected to `redirect_uri` with 615 a token; pass that token to `/api/v1/auth/verify/link` to complete registration and obtain 616 session tokens. 617 Profile fields (`full_name`, `alias`, `timezone`) and, with `set_org`, the `org` 618 details are captured now and applied when the link is verified; nothing is created 619 before the click. Requests are rate-limited per IP (10 per minute) and per 620 email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns 621 HTTP 204 on success. 622 623 Args: 624 alias: Display alias (handle) for the new account. 625 email: Email address to send the registration magic link to. 626 full_name: Full name for the new account. 627 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 628 redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter. 629 set_org: Create or reuse an organization from the work-email domain during confirmation. 630 timezone: IANA timezone name for the new account, e.g. `"America/New_York"`. 631 632 Returns: 633 No content 634 """ 635 body: dict[str, object] = {} 636 if alias is not None: 637 body["alias"] = alias 638 if email is not None: 639 body["email"] = email 640 if full_name is not None: 641 body["full_name"] = full_name 642 if org is not None: 643 body["org"] = org 644 if redirect_uri is not None: 645 body["redirect_uri"] = redirect_uri 646 if set_org is not None: 647 body["set_org"] = set_org 648 if timezone is not None: 649 body["timezone"] = timezone 650 651 data = self._http.request( 652 "/api/v1/auth/register/link", 653 method="POST", 654 body=body, 655 ) 656 return data
Request a magic link for registration
Starts a passwordless registration flow by sending a verification link to the given
email address. The recipient clicks the link and is redirected to redirect_uri with
a token; pass that token to /api/v1/auth/verify/link to complete registration and obtain
session tokens.
Profile fields (full_name, alias, timezone) and, with set_org, the org
details are captured now and applied when the link is verified; nothing is created
before the click. Requests are rate-limited per IP (10 per minute) and per
email-IP pair (3 per minute) exceeding either limit returns HTTP 429. Returns
HTTP 204 on success.
Arguments:
- alias: Display alias (handle) for the new account.
- email: Email address to send the registration magic link to.
- full_name: Full name for the new account.
- org: Company details for the organization a
set_orgregistration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). - redirect_uri: URL the user is redirected to after clicking the registration link. The token is appended as a query parameter.
- set_org: Create or reuse an organization from the work-email domain during confirmation.
- timezone: IANA timezone name for the new account, e.g.
"America/New_York".
Returns:
No content
658 def request_magic_link( 659 self, 660 email: str | None = None, 661 full_name: str | None = None, 662 mcp_client_id: str | None = None, 663 org: str | None = None, 664 redirect_uri: str | None = None, 665 set_org: str | None = None, 666 signup_origin: str | None = None, 667 ) -> dict: 668 """ 669 Request a magic link for login or registration 670 Sends a passwordless magic link to the given email address. If an account with that 671 email already exists, a login link is sent. If no account exists, a registration link 672 is sent and the recipient completes sign-up by clicking through. This unified endpoint 673 lets you implement a single email-entry UI that handles both cases transparently. 674 The `redirect_uri` is validated against the app's registered hosts; an unregistered 675 URI returns HTTP 400. Both `email` and `redirect_uri` are required. Requests are 676 rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns 677 HTTP 204 on success no body. If the app has Allowed Users enabled and the 678 address is not on the list, returns HTTP 403 `user_not_allowed`. That refusal 679 is the same for new and existing addresses, so it does not reveal whether an 680 account exists. 681 For a new user, `full_name` and (with `set_org`) the `org` details ride the pending 682 registration and are applied when the link is verified; nothing is created before 683 the click. They are ignored when the email already belongs to a user. Malformed 684 values return HTTP 422 before the address is looked up, so the response cannot 685 reveal whether an account exists. 686 687 Args: 688 email: Email address to send the magic link to. 689 full_name: Display name for the account the verified link will create. 690 mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key. 691 org: Company details for the organization a `set_org` registration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). 692 redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app. 693 set_org: For a new user, create or reuse an organization from the work-email domain during confirmation. 694 signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication. 695 696 Returns: 697 No content 698 """ 699 body: dict[str, object] = {} 700 if email is not None: 701 body["email"] = email 702 if full_name is not None: 703 body["full_name"] = full_name 704 if mcp_client_id is not None: 705 body["mcp_client_id"] = mcp_client_id 706 if org is not None: 707 body["org"] = org 708 if redirect_uri is not None: 709 body["redirect_uri"] = redirect_uri 710 if set_org is not None: 711 body["set_org"] = set_org 712 if signup_origin is not None: 713 body["signup_origin"] = signup_origin 714 715 data = self._http.request( 716 "/api/v1/auth/request/link", 717 method="POST", 718 body=body, 719 ) 720 return data
Request a magic link for login or registration
Sends a passwordless magic link to the given email address. If an account with that
email already exists, a login link is sent. If no account exists, a registration link
is sent and the recipient completes sign-up by clicking through. This unified endpoint
lets you implement a single email-entry UI that handles both cases transparently.
The redirect_uri is validated against the app's registered hosts; an unregistered
URI returns HTTP 400. Both email and redirect_uri are required. Requests are
rate-limited per IP (10 per minute) and per email-IP pair (3 per minute). Returns
HTTP 204 on success no body. If the app has Allowed Users enabled and the
address is not on the list, returns HTTP 403 user_not_allowed. That refusal
is the same for new and existing addresses, so it does not reveal whether an
account exists.
For a new user, full_name and (with set_org) the org details ride the pending
registration and are applied when the link is verified; nothing is created before
the click. They are ignored when the email already belongs to a user. Malformed
values return HTTP 422 before the address is looked up, so the response cannot
reveal whether an account exists.
Arguments:
- email: Email address to send the magic link to.
- full_name: Display name for the account the verified link will create.
- mcp_client_id: Opaque dynamic-registration client id for an MCP consent signup. Platform resolves it to a closed analytics key.
- org: Company details for the organization a
set_orgregistration creates at confirmation. Ignored when the domain org already exists or the mailbox is personal (which keeps the platform's own identity and takes only the sign-in methods). - redirect_uri: URL the user is redirected to after clicking the magic link. Must be registered with the app.
- set_org: For a new user, create or reuse an organization from the work-email domain during confirmation.
- signup_origin: Closed product intent for a new signup. Missing or unknown values become generic and never affect authentication.
Returns:
No content
722 def exchange_login_token(self, token: str, timezone: str | None = None) -> AuthTokens: 723 """ 724 Exchange a one-time login token for session tokens 725 Exchanges an email-login token (including email reply links) or an internal 726 impersonation login token for session credentials. Passwordless magic-link tokens 727 from `/api/v1/auth/login/link`, `/api/v1/auth/register/link`, or 728 `/api/v1/auth/request/link` must instead go to `/api/v1/auth/verify/link`. 729 Numeric email codes go to `/api/v1/auth/code/verify` with `email` and `code`. 730 Access and refresh tokens are not accepted here. Invalid, expired, or already-used 731 login tokens return HTTP 401 with structured `error.type`, `error.code`, and 732 `error.message` fields. 733 If `timezone` is provided and the user's current timezone is still the default 734 (`"America/Los_Angeles"`), the account timezone is updated in the same request. 735 Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429. 736 737 Args: 738 token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code. 739 timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g. `"Europe/London"`. Omit to leave the timezone unchanged. 740 741 Returns: 742 Access token, refresh token, and the authenticated user object. 743 """ 744 body: dict[str, object] = {} 745 body["token"] = token 746 if timezone is not None: 747 body["timezone"] = timezone 748 749 data = self._http.request( 750 "/api/v1/auth/token", 751 method="POST", 752 body=body, 753 ) 754 return AuthTokens( 755 token_expiry=data.get("expires_in"), 756 refresh_token=data.get("refresh_token"), 757 access_token=data.get("token"), 758 )
Exchange a one-time login token for session tokens
Exchanges an email-login token (including email reply links) or an internal
impersonation login token for session credentials. Passwordless magic-link tokens
from /api/v1/auth/login/link, /api/v1/auth/register/link, or
/api/v1/auth/request/link must instead go to /api/v1/auth/verify/link.
Numeric email codes go to /api/v1/auth/code/verify with email and code.
Access and refresh tokens are not accepted here. Invalid, expired, or already-used
login tokens return HTTP 401 with structured error.type, error.code, and
error.message fields.
If timezone is provided and the user's current timezone is still the default
("America/Los_Angeles"), the account timezone is updated in the same request.
Requests are rate-limited to 10 per IP per minute; exceeding this returns HTTP 429.
Arguments:
- token: Single-use email-login or impersonation token; not a passwordless magic-link token or numeric code.
- timezone: IANA timezone name to apply to the account if the account timezone is still the default, e.g.
"Europe/London". Omit to leave the timezone unchanged.
Returns:
Access token, refresh token, and the authenticated user object.
760 def verify_magic_link(self, token: str | None = None) -> AuthTokens: 761 """ 762 Verify a magic link token 763 Consumes a single-use token from a magic link URL and returns an access token, 764 refresh token, and the authenticated user object. This endpoint completes both the 765 login flow (initiated by `/api/v1/auth/login/link`) and the registration flow 766 (initiated by `/api/v1/auth/register/link` or `/api/v1/auth/request/link`). 767 Extract the token from the `token` query parameter of the magic link redirect URI 768 and POST JSON `{"token":"<token from redirect URI>"}` to 769 `/api/v1/auth/verify/link`, not `/api/v1/auth/token`. Expired or already-used tokens return HTTP 401 expired links 770 have `error.code` = `authentication_error` and `error.message` = 771 `expired_token`; unknown or already-used tokens have `error.message` = 772 `invalid_or_expired_token`. If the app has disabled passwordless authentication 773 the request returns HTTP 403. Rate-limited to 10 requests per IP per minute 774 exceeding this returns HTTP 429. 775 776 Args: 777 token: Single-use magic link token extracted from the redirect URI query parameter. 778 779 Returns: 780 Access token, refresh token, and the authenticated user object. 781 """ 782 body: dict[str, object] = {} 783 if token is not None: 784 body["token"] = token 785 786 data = self._http.request( 787 "/api/v1/auth/verify/link", 788 method="POST", 789 body=body, 790 ) 791 return AuthTokens( 792 token_expiry=data.get("expires_in"), 793 refresh_token=data.get("refresh_token"), 794 access_token=data.get("token"), 795 )
Verify a magic link token
Consumes a single-use token from a magic link URL and returns an access token,
refresh token, and the authenticated user object. This endpoint completes both the
login flow (initiated by /api/v1/auth/login/link) and the registration flow
(initiated by /api/v1/auth/register/link or /api/v1/auth/request/link).
Extract the token from the token query parameter of the magic link redirect URI
and POST JSON {"token":"<token from redirect URI>"} to
/api/v1/auth/verify/link, not /api/v1/auth/token. Expired or already-used tokens return HTTP 401 expired links
have error.code = authentication_error and error.message =
expired_token; unknown or already-used tokens have error.message =
invalid_or_expired_token. If the app has disabled passwordless authentication
the request returns HTTP 403. Rate-limited to 10 requests per IP per minute
exceeding this returns HTTP 429.
Arguments:
- token: Single-use magic link token extracted from the redirect URI query parameter.
Returns:
Access token, refresh token, and the authenticated user object.