Skip to content

Authentication API

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.

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:

ArgumentTypeRequired
inputSignInInput!Yes

SignInInput:

FieldTypeNotes
emailStringProvide either email or phoneNumber
phoneNumberStringProvide either email or phoneNumber
passwordString!Required

Returns: AccessToken!

FieldType
accessTokenString!
expiresInFloat!
refreshTokenString!
refreshExpiresInFloat!

Example request:

mutation Login($input: SignInInput!) {
login(input: $input) {
accessToken
expiresIn
refreshToken
refreshExpiresIn
}
}
{
"input": {
"email": "[email protected]",
"password": "correct-horse-battery-staple"
}
}
Terminal window
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
}
}
}

Exchanges a still-valid refresh token for a new access/refresh token pair.

Auth required: None (the refresh token itself is the credential).

Arguments:

ArgumentTypeRequired
inputRefreshAccessTokenInput!Yes

RefreshAccessTokenInput:

FieldTypeNotes
refreshTokenString!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>" } }

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:

ArgumentTypeRequired
emailString!Yes
idNoStringNo

Returns: JSONObject! (a free-form JSON result describing registerability)

Example:

query IsRegisterable($email: String!) {
isRegisterable(email: $email)
}
{ "email": "[email protected]" }

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:

ArgumentTypeRequired
inputRegisterInput!Yes
tokenValidateSecuredTokenInputNo — defaults to null

RegisterInput:

FieldTypeNotes
emailString!Required
passwordString!Required
fullnameString!Required
phoneCodeString!Required, e.g. "+1"
phoneNumberString!Required
fcmTokenStringOptional, for push notifications
idNoStringOptional
idTypeStringOptional
referralCodeStringOptional

ValidateSecuredTokenInput (optional, used when registration is gated behind an OTP/invite token):

FieldTypeNotes
tokenString!The secured token or secured OTP string
contactStringContact info tied to the token (email/phone); optional for SECURED_TOKEN
idFloatPreferred 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": {
"email": "[email protected]",
"password": "correct-horse-battery-staple",
"fullname": "Jamie Rivera",
"phoneCode": "+1",
"phoneNumber": "5551234567"
}
}

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.

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:

  1. If this WebbyX One identity is already linked to a Wetel account (UserIdentityEntity(provider: WEBBYX_ONE)), log into that account.
  2. 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.
  3. 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:

ArgumentTypeRequired
ticketString!Yes

Returns: AccessToken! (same shape as login)

Example:

mutation LoginWithWebbyxOne($ticket: String!) {
loginWithWebbyxOne(ticket: $ticket) {
accessToken
expiresIn
refreshToken
refreshExpiresIn
}
}
{ "ticket": "tk_a1b2c3..." }

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:

ArgumentTypeRequired
providerAuthProvider!Yes
providerTokenString!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..." }
Terminal window
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..."}}'

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:

ArgumentTypeRequired
providerAuthProvider!Yes

Returns: Boolean!

Example:

mutation UnlinkIdentity($provider: AuthProvider!) {
unlinkIdentity(provider: $provider)
}
{ "provider": "WEBBYX_ONE" }

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:

ArgumentTypeRequired
billingAccountPrimaryBillingAccountEnum!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" }
Terminal window
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"}}'

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:

ArgumentTypeRequired
inputGenerateOTPInput!Yes

GenerateOTPInput:

FieldTypeNotes
emailStringProvide either email or phoneNumber
phoneNumberStringProvide either email or phoneNumber
purposeTokenPurpose!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)
}
{ "input": { "email": "[email protected]", "purpose": "RESET_PASSWORD" } }

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:

ArgumentTypeRequired
inputResetPasswordEmailInput!Yes

ResetPasswordEmailInput:

FieldTypeNotes
emailStringOptional — omitting it still returns true
purposeTokenPurpose!Typically RESET_PASSWORD

Returns: Boolean!

Example:

mutation ResetPasswordEmail($input: ResetPasswordEmailInput!) {
resetUserPasswordWithUrl(input: $input)
}
{ "input": { "email": "[email protected]", "purpose": "RESET_PASSWORD" } }

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:

ArgumentTypeRequired
inputPasswordWithOTPInput!Yes

PasswordWithOTPInput:

FieldTypeNotes
newPasswordString!Required
tokenValidateSecuredTokenInput!See shape under register above

Returns: Boolean!

Example:

mutation ForgotPasswordWithOTP($input: PasswordWithOTPInput!) {
forgotPasswordWithOTP(input: $input)
}
{
"input": {
"newPassword": "new-correct-horse-battery-staple",
"token": { "token": "123456", "contact": "[email protected]" }
}
}

Changes the password for the currently authenticated user.

Auth required: JWT (AuthJwtGuard).

Arguments:

ArgumentTypeRequired
inputPasswordInput!Yes

PasswordInput:

FieldTypeNotes
newPasswordString!Required

Returns: Boolean!

Example:

mutation ChangePassword($input: PasswordInput!) {
changePassword(input: $input)
}
{ "input": { "newPassword": "new-correct-horse-battery-staple" } }
Terminal window
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"}}}'

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!

FieldTypeNotes
secretString!The raw TOTP secret
outputString!QR-code-ready payload (e.g. otpauth:// URI)

Example:

mutation GenerateTwoFactor {
generateTwoFactor {
secret
output
}
}

Binds 2FA to the current user by confirming a code generated from the secret returned by generateTwoFactor.

Auth required: JWT (AuthJwtGuard).

Arguments:

ArgumentTypeRequired
inputBindTwoFactorInput!Yes

BindTwoFactorInput:

FieldTypeNotes
secretString!The secret from generateTwoFactor
codeString!The current TOTP code generated from that secret

Returns: Boolean!

Example:

mutation BindTwoFactor($input: BindTwoFactorInput!) {
bindTwoFactor(input: $input)
}
{ "input": { "secret": "JBSWY3DPEHPK3PXP", "code": "482913" } }

Verifies a 2FA code. Per the schema’s own description, most integrations will not need to call this directly.

Auth required: JWT (AuthJwtGuard).

Arguments:

ArgumentTypeRequired
codeString!Yes

Returns: Boolean!

Example:

mutation VerifyTwoFactor($code: String!) {
verifyTwoFactor(code: $code)
}
{ "code": "482913" }

Removes 2FA from the current user’s account.

Auth required: JWT (AuthJwtGuard).

Arguments:

ArgumentTypeRequired
inputUnbindTwoFactorInput!Yes

UnbindTwoFactorInput:

FieldTypeNotes
codeString!Current TOTP code, required to confirm the unbind

Returns: Boolean!

Example:

mutation UnbindTwoFactor($input: UnbindTwoFactorInput!) {
unbindTwoFactor(input: $input)
}
{ "input": { "code": "482913" } }

Retrieves the profile of the currently authenticated user.

Auth required: JWT (AuthJwtGuard).

Arguments: None.

Returns: User (nullable)

FieldTypeNotes
idInt!
emailString
fullnameString!
usernameString
phoneCodeString
phoneNumberString
idNoString
idTypeString
referralCodeString!
statusUserStatusType!ACTIVE or SUSPENDED
twoFactorEnabledBoolean!Derived from whether a 2FA secret is bound — the secret itself is never exposed
createdAtDateTime!
updatedAtDateTime!

Example request:

query GetAuthProfile {
getAuthProfile {
id
email
fullname
status
twoFactorEnabled
}
}
Terminal window
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,
"email": "[email protected]",
"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.