Skip to content

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.

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.