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        )
@dataclass
class AuthTokens:
13@dataclass
14class AuthTokens:
15    token_expiry: int | None = None
16    refresh_token: str | None = None
17    access_token: str | None = None
AuthTokens( token_expiry: int | None = None, refresh_token: str | None = None, access_token: str | None = None)
token_expiry: int | None = None
refresh_token: str | None = None
access_token: str | None = None
class AsyncAuthClient:
 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        )
AsyncAuthClient(http: archastro.platform.runtime.http_client.HttpClient)
21    def __init__(self, http: HttpClient):
22        self._http = http
async def allowed_auth_methods(self) -> dict:
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

async def login(self, email: str, password: str) -> AuthTokens:
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.

async def refresh(self, refresh_token: str) -> AuthTokens:
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.

async def register( self, email: str, alias: str | None = None, full_name: str | None = None, invite_code: str | None = None, password: str | None = None, set_org: str | None = None, team_invite: str | None = None, timezone: str | None = None) -> AuthTokens:
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_invite with 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. An invite_code may optionally be included for invite-gated apps; an invalid code returns HTTP 404. Provide team_invite or password; when both are supplied, team registration takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 with field-level guidance. Use full_name for the profile name (returned as user.name); name is not an input field. alias is a separate optional handle. Legacy team_invite_id remains accepted; team_invite takes 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.

async def exchange_login_token( self, token: str, timezone: str | None = None) -> AuthTokens:
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.

class AuthClient:
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        )
410    def __init__(self, http: SyncHttpClient):
411        self._http = http
def allowed_auth_methods(self) -> dict:
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

def login(self, email: str, password: str) -> AuthTokens:
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.

def refresh(self, refresh_token: str) -> AuthTokens:
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.

def register( self, email: str, alias: str | None = None, full_name: str | None = None, invite_code: str | None = None, password: str | None = None, set_org: str | None = None, team_invite: str | None = None, timezone: str | None = None) -> AuthTokens:
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_invite with 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. An invite_code may optionally be included for invite-gated apps; an invalid code returns HTTP 404. Provide team_invite or password; when both are supplied, team registration takes precedence. Omitting both returns HTTP 400. Unknown fields return HTTP 400 with field-level guidance. Use full_name for the profile name (returned as user.name); name is not an input field. alias is a separate optional handle. Legacy team_invite_id remains accepted; team_invite takes 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.

def exchange_login_token( self, token: str, timezone: str | None = None) -> AuthTokens:
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.