Authentication API
This content is not available in your language yet.
This page documents every operation in the authentication flow. See Authentication for the conceptual overview (JWT structure, x-huat-platform header, token lifecycle) — this page focuses on exact arguments, return shapes, and examples.
Every request in this reference — authenticated or not — must include the x-huat-platform: customer header. Requests without it are rejected before they reach any resolver.
Most operations on this page require no prior authentication — they are how a caller obtains a JWT in the first place. Each operation below states its actual auth requirement explicitly.
Signing in
Section titled “Signing in”Authenticates a user with email/phone + password and returns an access/refresh token pair.
Auth required: None. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled.
Arguments:
| Argument | Type | Required |
|---|---|---|
input | SignInInput! | Yes |
SignInInput:
| Field | Type | Notes |
|---|---|---|
email | String | Provide either email or phoneNumber |
phoneNumber | String | Provide either email or phoneNumber |
password | String! | Required |
Returns: AccessToken!
| Field | Type |
|---|---|
accessToken | String! |
expiresIn | Float! |
refreshToken | String! |
refreshExpiresIn | Float! |
Example request:
mutation Login($input: SignInInput!) { login(input: $input) { accessToken expiresIn refreshToken refreshExpiresIn }}{ "input": { "password": "correct-horse-battery-staple" }}curl https://api.wetel.dev/graphql \ -H "Content-Type: application/json" \ -H "x-huat-platform: customer" \ -d '{"query":"mutation Login($input: SignInInput!) { login(input: $input) { accessToken expiresIn refreshToken refreshExpiresIn } }","variables":{"input":{"email":"[email protected]","password":"correct-horse-battery-staple"}}}'Example response:
{ "data": { "login": { "accessToken": "<YOUR_API_KEY>", "expiresIn": 3600, "refreshToken": "<YOUR_API_KEY>", "refreshExpiresIn": 2592000 } }}refreshToken
Section titled “refreshToken”Exchanges a still-valid refresh token for a new access/refresh token pair.
Auth required: None (the refresh token itself is the credential).
Arguments:
| Argument | Type | Required |
|---|---|---|
input | RefreshAccessTokenInput! | Yes |
RefreshAccessTokenInput:
| Field | Type | Notes |
|---|---|---|
refreshToken | String! | Must match the wallet/session that originally issued it |
Returns: AccessToken! (same shape as login)
Example:
mutation RefreshToken($input: RefreshAccessTokenInput!) { refreshToken(input: $input) { accessToken expiresIn refreshToken refreshExpiresIn }}{ "input": { "refreshToken": "<YOUR_API_KEY>" } }Registering
Section titled “Registering”isRegisterable
Section titled “isRegisterable”Checks whether an email is available for registration.
Auth required: None. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled.
Arguments:
| Argument | Type | Required |
|---|---|---|
email | String! | Yes |
idNo | String | No |
Returns: JSONObject! (a free-form JSON result describing registerability)
Example:
query IsRegisterable($email: String!) { isRegisterable(email: $email)}register
Section titled “register”Creates a new user account and returns an access/refresh token pair immediately.
Auth required: None. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled.
Arguments:
| Argument | Type | Required |
|---|---|---|
input | RegisterInput! | Yes |
token | ValidateSecuredTokenInput | No — defaults to null |
RegisterInput:
| Field | Type | Notes |
|---|---|---|
email | String! | Required |
password | String! | Required |
fullname | String! | Required |
phoneCode | String! | Required, e.g. "+1" |
phoneNumber | String! | Required |
fcmToken | String | Optional, for push notifications |
idNo | String | Optional |
idType | String | Optional |
referralCode | String | Optional |
ValidateSecuredTokenInput (optional, used when registration is gated behind an OTP/invite token):
| Field | Type | Notes |
|---|---|---|
token | String! | The secured token or secured OTP string |
contact | String | Contact info tied to the token (email/phone); optional for SECURED_TOKEN |
id | Float | Preferred when the token type is TIMED_OTP; use contact when the frontend has no id |
Returns: AccessToken!
A freshly registered user is automatically assigned a new tenant of their own (named "<fullname>'s Workspace") — myTenant returns this new tenant immediately, not null. If you’re calling register from a flow that expects to explicitly create or assign a tenant afterward, note that one already exists by the time this mutation returns; check myTenant first rather than assuming you need to provision one yourself.
Example:
mutation Register($input: RegisterInput!) { register(input: $input) { accessToken refreshToken expiresIn }}{ "input": { "password": "correct-horse-battery-staple", "fullname": "Jamie Rivera", "phoneCode": "+1", "phoneNumber": "5551234567" }}Login with WebbyX One
Section titled “Login with WebbyX One”These three mutations let a person log into the Wetel dashboard using their WebbyX One (IAM One) account — either instead of email/password on first contact, or in addition to it from account settings. This is purely additive: it never replaces or disables password (or any other) login for an account.
loginWithWebbyxOne
Section titled “loginWithWebbyxOne”Logs in — or, on a genuinely first-time WebbyX One identity, silently creates — a Wetel dashboard account via WebbyX One SSO.
ticket is the single-use ticket minted by WebbyX One’s own /sso/login step (a frontend concern — your own sign-in page calls that endpoint with the public X-Client-Id, the same way sdkVerifyWebbyxOneIdentity’s docs describe; this mutation only consumes the resulting ticket). The actual token exchange happens server-side on Wetel’s end.
Account/tenant resolution, in order:
- If this WebbyX One identity is already linked to a Wetel account (
UserIdentityEntity(provider: WEBBYX_ONE)), log into that account. - Otherwise, if the ticket’s verified email matches an existing Wetel account (any login method — password, or a different provider), the WebbyX One identity is auto-linked to that account, and its existing tenant is used. An existing tenant always wins — no new company-based tenant is created or attached in this case.
- Otherwise (genuinely no existing account at all), a new Wetel account is created, and it’s attached to a tenant keyed by the WebbyX One company: if another user from the same company has already logged in via WebbyX One before, they join that same tenant; if not, a new tenant is created. Multiple people from the same WebbyX One company end up sharing one Wetel tenant, the same way an invited teammate does today.
There is no confirmation/consent step on this first-time path — a successful ticket exchange for a brand-new identity creates the account and logs it in immediately.
Auth required: None. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled.
Arguments:
| Argument | Type | Required |
|---|---|---|
ticket | String! | Yes |
Returns: AccessToken! (same shape as login)
Example:
mutation LoginWithWebbyxOne($ticket: String!) { loginWithWebbyxOne(ticket: $ticket) { accessToken expiresIn refreshToken refreshExpiresIn }}{ "ticket": "tk_a1b2c3..." }linkIdentity
Section titled “linkIdentity”Links an additional login provider to the currently-authenticated account — e.g. an existing password user adds WebbyX One from account settings.
providerToken is provider-specific: for provider: WEBBYX_ONE, it’s a ticket, the same shape loginWithWebbyxOne accepts. Only WEBBYX_ONE is implemented today — GOOGLE/GITHUB are reserved AuthProvider values for future providers and throw a not-implemented error if used. Idempotent for re-linking the same provider to the same account; refuses if the identity is already linked to a different Wetel account.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
provider | AuthProvider! | Yes |
providerToken | String! | Yes |
AuthProvider values: PASSWORD | WEBBYX_ONE | GOOGLE | GITHUB (only PASSWORD and WEBBYX_ONE are functional today).
Returns: Boolean!
Example:
mutation LinkIdentity($provider: AuthProvider!, $providerToken: String!) { linkIdentity(provider: $provider, providerToken: $providerToken)}{ "provider": "WEBBYX_ONE", "providerToken": "tk_a1b2c3..." }curl https://api.wetel.dev/graphql \ -H "Content-Type: application/json" \ -H "x-huat-platform: customer" \ -H "Authorization: Bearer <YOUR_API_KEY>" \ -d '{"query":"mutation LinkIdentity($provider: AuthProvider!, $providerToken: String!) { linkIdentity(provider: $provider, providerToken: $providerToken) }","variables":{"provider":"WEBBYX_ONE","providerToken":"tk_a1b2c3..."}}'unlinkIdentity
Section titled “unlinkIdentity”Removes a linked login provider from the currently-authenticated account. Refuses (throws) if this would leave the account with zero login methods — every account must always retain at least one.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
provider | AuthProvider! | Yes |
Returns: Boolean!
Example:
mutation UnlinkIdentity($provider: AuthProvider!) { unlinkIdentity(provider: $provider)}{ "provider": "WEBBYX_ONE" }setPrimaryBillingAccount
Section titled “setPrimaryBillingAccount”Sets which system bills the currently-authenticated account’s tenant — WEBBYX_ONE (WebbyX One’s own credit ledger) or WETEL (Wetel’s own billing; re-enables the tenant’s normal per-plan monthly LLM spend cap). This is explicit and user-set — never inferred from which login method was used for a given session, and never automatic just because a WebbyX One identity happens to be linked.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
billingAccount | PrimaryBillingAccountEnum! | Yes |
PrimaryBillingAccountEnum values: WEBBYX_ONE | WETEL.
Returns: TenantDto! (see Tenant API for the full shape — primaryBillingAccount reflects the value just set)
Example:
mutation SetPrimaryBillingAccount($billingAccount: PrimaryBillingAccountEnum!) { setPrimaryBillingAccount(billingAccount: $billingAccount) { id primaryBillingAccount }}{ "billingAccount": "WEBBYX_ONE" }curl https://api.wetel.dev/graphql \ -H "Content-Type: application/json" \ -H "x-huat-platform: customer" \ -H "Authorization: Bearer <YOUR_API_KEY>" \ -d '{"query":"mutation SetPrimaryBillingAccount($billingAccount: PrimaryBillingAccountEnum!) { setPrimaryBillingAccount(billingAccount: $billingAccount) { id primaryBillingAccount } }","variables":{"billingAccount":"WEBBYX_ONE"}}'Password reset
Section titled “Password reset”generateOTP
Section titled “generateOTP”Generates a one-time password for a given purpose (sign-in, sign-up, password reset, etc.).
Auth required: None. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled.
Arguments:
| Argument | Type | Required |
|---|---|---|
input | GenerateOTPInput! | Yes |
GenerateOTPInput:
| Field | Type | Notes |
|---|---|---|
email | String | Provide either email or phoneNumber |
phoneNumber | String | Provide either email or phoneNumber |
purpose | TokenPurpose! | One of SIGN_IN, SIGN_UP, RESET_PASSWORD, INVITATION, UPDATE_PROFILE |
Returns: Int! (an identifier for the generated OTP record — pass this or the matching contact info into validateSecuredToken / the OTP-based password reset)
As of 2026-09-08, a generated code is purpose-bound and lockout-protected: a code minted for one purpose can only be spent against the flow matching that same purpose (e.g. a RESET_PASSWORD code can’t be validated by anything other than the reset-password flow), and a code locks out after 5 wrong-code attempts. A locked-out, wrong, and unknown code all return the same generic validation error — a caller can’t distinguish which case they hit. Request a fresh OTP if validation keeps failing.
Example:
mutation GenerateOTP($input: GenerateOTPInput!) { generateOTP(input: $input)}resetUserPasswordWithUrl
Section titled “resetUserPasswordWithUrl”Sends a password-reset email containing a reset link/token.
Auth required: None. Always returns true regardless of whether the email exists, to avoid leaking which emails are registered. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled.
Arguments:
| Argument | Type | Required |
|---|---|---|
input | ResetPasswordEmailInput! | Yes |
ResetPasswordEmailInput:
| Field | Type | Notes |
|---|---|---|
email | String | Optional — omitting it still returns true |
purpose | TokenPurpose! | Typically RESET_PASSWORD |
Returns: Boolean!
Example:
mutation ResetPasswordEmail($input: ResetPasswordEmailInput!) { resetUserPasswordWithUrl(input: $input)}forgotPasswordWithOTP
Section titled “forgotPasswordWithOTP”Sets a new password using a previously validated OTP/secured token — used for the “forgot password” flow (no current password required).
Auth required: None. Rate-limited (as of 2026-09-08) — repeated calls from the same source IP are throttled. The presented token must have been generated with purpose: RESET_PASSWORD — a token minted for a different purpose (e.g. SIGN_IN) is now rejected rather than accepted.
Arguments:
| Argument | Type | Required |
|---|---|---|
input | PasswordWithOTPInput! | Yes |
PasswordWithOTPInput:
| Field | Type | Notes |
|---|---|---|
newPassword | String! | Required |
token | ValidateSecuredTokenInput! | See shape under register above |
Returns: Boolean!
Example:
mutation ForgotPasswordWithOTP($input: PasswordWithOTPInput!) { forgotPasswordWithOTP(input: $input)}{ "input": { "newPassword": "new-correct-horse-battery-staple", }}changePassword
Section titled “changePassword”Changes the password for the currently authenticated user.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
input | PasswordInput! | Yes |
PasswordInput:
| Field | Type | Notes |
|---|---|---|
newPassword | String! | Required |
Returns: Boolean!
Example:
mutation ChangePassword($input: PasswordInput!) { changePassword(input: $input)}{ "input": { "newPassword": "new-correct-horse-battery-staple" } }curl https://api.wetel.dev/graphql \ -H "Content-Type: application/json" \ -H "x-huat-platform: customer" \ -H "Authorization: Bearer <YOUR_API_KEY>" \ -d '{"query":"mutation ChangePassword($input: PasswordInput!) { changePassword(input: $input) }","variables":{"input":{"newPassword":"new-correct-horse-battery-staple"}}}'Two-factor authentication
Section titled “Two-factor authentication”generateTwoFactor
Section titled “generateTwoFactor”Generates a new 2FA secret and QR-code payload for the current user to scan into an authenticator app, prior to binding it.
Auth required: JWT (AuthJwtGuard).
Arguments: None.
Returns: TwoFactorInfo!
| Field | Type | Notes |
|---|---|---|
secret | String! | The raw TOTP secret |
output | String! | QR-code-ready payload (e.g. otpauth:// URI) |
Example:
mutation GenerateTwoFactor { generateTwoFactor { secret output }}bindTwoFactor
Section titled “bindTwoFactor”Binds 2FA to the current user by confirming a code generated from the secret returned by generateTwoFactor.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
input | BindTwoFactorInput! | Yes |
BindTwoFactorInput:
| Field | Type | Notes |
|---|---|---|
secret | String! | The secret from generateTwoFactor |
code | String! | The current TOTP code generated from that secret |
Returns: Boolean!
Example:
mutation BindTwoFactor($input: BindTwoFactorInput!) { bindTwoFactor(input: $input)}{ "input": { "secret": "JBSWY3DPEHPK3PXP", "code": "482913" } }verifyTwoFactor
Section titled “verifyTwoFactor”Verifies a 2FA code. Per the schema’s own description, most integrations will not need to call this directly.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
code | String! | Yes |
Returns: Boolean!
Example:
mutation VerifyTwoFactor($code: String!) { verifyTwoFactor(code: $code)}{ "code": "482913" }unbindTwoFactor
Section titled “unbindTwoFactor”Removes 2FA from the current user’s account.
Auth required: JWT (AuthJwtGuard).
Arguments:
| Argument | Type | Required |
|---|---|---|
input | UnbindTwoFactorInput! | Yes |
UnbindTwoFactorInput:
| Field | Type | Notes |
|---|---|---|
code | String! | Current TOTP code, required to confirm the unbind |
Returns: Boolean!
Example:
mutation UnbindTwoFactor($input: UnbindTwoFactorInput!) { unbindTwoFactor(input: $input)}{ "input": { "code": "482913" } }Current user profile
Section titled “Current user profile”getAuthProfile
Section titled “getAuthProfile”Retrieves the profile of the currently authenticated user.
Auth required: JWT (AuthJwtGuard).
Arguments: None.
Returns: User (nullable)
| Field | Type | Notes |
|---|---|---|
id | Int! | |
email | String | |
fullname | String! | |
username | String | |
phoneCode | String | |
phoneNumber | String | |
idNo | String | |
idType | String | |
referralCode | String! | |
status | UserStatusType! | ACTIVE or SUSPENDED |
twoFactorEnabled | Boolean! | Derived from whether a 2FA secret is bound — the secret itself is never exposed |
createdAt | DateTime! | |
updatedAt | DateTime! |
Example request:
query GetAuthProfile { getAuthProfile { id email fullname status twoFactorEnabled }}curl https://api.wetel.dev/graphql \ -H "Content-Type: application/json" \ -H "x-huat-platform: customer" \ -H "Authorization: Bearer <YOUR_API_KEY>" \ -d '{"query":"query { getAuthProfile { id email fullname status twoFactorEnabled } }"}'Example response:
{ "data": { "getAuthProfile": { "id": 42, "fullname": "Jamie Rivera", "status": "ACTIVE", "twoFactorEnabled": false } }}Once you have an accessToken, see Tenant for reading/managing your tenant and generating an API key for server-to-server calls, and Getting Started for the overall flow from signup to your first agent.