Skip to main content

Message Contracts

All messages are listed below, grouped by direction. For enums (PRIVATE_KIT_MESSAGE_TYPES, USERNAME_VALIDATION_REASON, EMAIL_VALIDATION_REASON, EMAIL_CONFIRMATION_REASON, PHONE_VALIDATION_REASON, PHONE_CONFIRMATION_REASON, PASSWORD_VALIDATION_REASON) see Reference.

iframe → parent

PRIVATE_KIT_INIT

Emitted once when the iframe mounts.

{
type: 'PRIVATE_KIT_INIT',
payload: {
connectionId: string; // UUID generated by the iframe
}
}

Host action: save connectionId, mark the iframe as ready.

Note that — unlike Web3Kit — PRIVATE_KIT_INIT does not carry an authToken. The token travels inside each action message instead.

PRIVATE_KIT_USERNAME_UPDATED

Successful response to PRIVATE_KIT_UPDATE_USERNAME. The username on the backend now equals payload.username.

{
type: 'PRIVATE_KIT_USERNAME_UPDATED',
payload: {
connectionId: string;
username: string; // the value that was persisted (already trimmed)
}
}

Host action: update local UI / cached profile and stop showing the form, or signal success in whatever way fits your flow.

PRIVATE_KIT_USERNAME_VALIDATION_ERROR

Any negative outcome that is not a 401 — both client-side rule violations and the "username is taken" case.

{
type: 'PRIVATE_KIT_USERNAME_VALIDATION_ERROR',
payload: {
connectionId: string;
reason: USERNAME_VALIDATION_REASON; // 'required' | 'invalid' | 'exist' | 'unknown'
message?: string; // present for 'unknown' — the original error message, best effort
}
}

Reason mapping:

reasonMeaning
requiredThe trimmed username is empty, or the message envelope was missing username / authToken.
invalidUsername failed userNameValidation — must be ≥ 5 characters, alphanumeric, and contain at least one letter.
existPOST /private/api/v1/users/exists returned isExistsUsername: true — the name is already taken.
unknownAn unexpected non-401 error from GET /private/api/v1/users, POST /private/api/v1/users/exists, or setUsername.

Note that submitting the user's current username is not an error — the iframe replies with PRIVATE_KIT_USERNAME_UPDATED and skips the API call. See PRIVATE_KIT_UPDATE_USERNAME below.

Host action: show the corresponding inline error next to the input. The contract types in src/components/Form accept a translation key as the error string, so a host that wants the standard auth-central wording can map required / invalid to account.changeUsernameForm.{reason} and exist to account.changeUsernameForm.tryAnotherUsername.

PRIVATE_KIT_EMAIL_UPDATED

Successful response to PRIVATE_KIT_UPDATE_EMAIL. The backend has accepted the change request and sent a verification code to the new mailbox. The change is not committed yet — the user must follow up with PRIVATE_KIT_CONFIRM_EMAIL.

{
type: 'PRIVATE_KIT_EMAIL_UPDATED',
payload: {
connectionId: string;
email: string; // the value that was submitted to /setEmail (already trimmed)
}
}

Host action: switch the UI to the verification step (input for the code, "Resend code" button).

PRIVATE_KIT_EMAIL_VALIDATION_ERROR

Any non-401 negative outcome from PRIVATE_KIT_UPDATE_EMAIL or PRIVATE_KIT_RESEND_EMAIL_CODE — client-side rule violations, "email is taken", and backend rate-limit responses.

{
type: 'PRIVATE_KIT_EMAIL_VALIDATION_ERROR',
payload: {
connectionId: string;
reason: EMAIL_VALIDATION_REASON; // 'required' | 'invalid' | 'exist' | 'limitReached' | 'unknown'
message?: string;
}
}

Reason mapping:

reasonMeaning
requiredThe trimmed email is empty, or the message envelope was missing email / authToken. Also used by RESEND_EMAIL_CODE when authToken is missing.
invalidEmail failed emailValidation — basic [email protected] regex check.
existPOST /private/api/v1/users/exists returned isExistsEmail: true — the address is already taken by another user.
limitReachedBackend responded with 400 from setEmail or resendEmail/{id} — usually a verification-send rate limit. Mirror of the "limit reached" toast in the standalone flow.
unknownAny other non-401 error from GET /users, POST /users/exists, setEmail, or resendEmail/{id}.

Note that submitting the user's current email is not an error — the iframe replies with PRIVATE_KIT_EMAIL_UPDATED and skips the API call. See PRIVATE_KIT_UPDATE_EMAIL below.

Host action: show inline error next to the field. Recommended translation-key mappings: required / invalidaccount.changeEmailForm.{reason}, existaccount.changeEmailForm.tryAnotherEmail, limitReachedcommon.errors.limitReached.

PRIVATE_KIT_EMAIL_CONFIRMED

Successful response to PRIVATE_KIT_CONFIRM_EMAIL. The new email is now the user's primary email on the backend, and the backend has rotated the session JWTs as part of the confirmation.

{
type: 'PRIVATE_KIT_EMAIL_CONFIRMED',
payload: {
connectionId: string;
email: string; // the freshly-confirmed email (echoed from the backend response)
token: string; // new access JWT
refreshToken: string; // new refresh JWT
}
}

Host action: store the new tokens (typically by writing them to the same cookie / storage that drives your private API calls), update the cached profile, and close the iframe or move to the next step in your UX.

PRIVATE_KIT_EMAIL_CONFIRMATION_ERROR

Any non-401 negative outcome of PRIVATE_KIT_CONFIRM_EMAIL.

{
type: 'PRIVATE_KIT_EMAIL_CONFIRMATION_ERROR',
payload: {
connectionId: string;
reason: EMAIL_CONFIRMATION_REASON; // 'required' | 'max' | 'invalid' | 'invalidCode' | 'unknown'
message?: string;
}
}

Reason mapping (mirrors emailVerificationFormConfig rule-by-rule, so the host can map straight onto its translation keys):

reasonMeaning
requiredconfirmationCode is empty, or the envelope was missing confirmationCode / authToken.
maxThe code is longer than 6 characters.
invalidThe code contains non-digit characters (does not match /^\d+$/).
invalidCodeBackend responded 400 to POST /private/api/v1/verification/confirm/{id} — the code is wrong or expired.
unknownAny other non-401 error from GET /users or verification/confirm/{id}.

Host action: show inline error next to the code input. Recommended translation-key mappings: requiredaccount.EMAIL_VERIFICATION.verificationCode.validation.required, maxaccount.EMAIL_VERIFICATION.verificationCode.validation.max, invalidaccount.EMAIL_VERIFICATION.verificationCode.validation.isNumber, invalidCodeaccount.EMAIL_VERIFICATION.invalidCode.

PRIVATE_KIT_EMAIL_CODE_RESENT

Successful response to PRIVATE_KIT_RESEND_EMAIL_CODE. The backend has dispatched a fresh verification code to the pending email.

{
type: 'PRIVATE_KIT_EMAIL_CODE_RESENT',
payload: {
connectionId: string;
}
}

Host action: show a "code resent" toast / hint; the verification input stays open.

PRIVATE_KIT_PHONE_UPDATED

Successful response to PRIVATE_KIT_UPDATE_PHONE. The backend has accepted the change request and sent an SMS code to the new number. The change is not committed yet — the user must follow up with PRIVATE_KIT_CONFIRM_PHONE.

{
type: 'PRIVATE_KIT_PHONE_UPDATED',
payload: {
connectionId: string;
phoneNumber: string; // the value that was submitted to /setPhone (already trimmed)
}
}

Host action: switch the UI to the verification step (input for the code, "Resend code" button).

PRIVATE_KIT_PHONE_VALIDATION_ERROR

Any non-401 negative outcome from PRIVATE_KIT_UPDATE_PHONE or PRIVATE_KIT_RESEND_PHONE_CODE — client-side rule violations, "number is taken", and backend rate-limit responses.

{
type: 'PRIVATE_KIT_PHONE_VALIDATION_ERROR',
payload: {
connectionId: string;
reason: PHONE_VALIDATION_REASON; // 'required' | 'invalid' | 'exist' | 'limitReached' | 'unknown'
message?: string;
}
}

Reason mapping:

reasonMeaning
requiredThe trimmed phoneNumber is empty, or the message envelope was missing phoneNumber / authToken. Also used by RESEND_PHONE_CODE when authToken is missing.
invalidphoneNumber failed phoneValidation (google-libphonenumber). Send numbers in international format with a + prefix.
existPOST /private/api/v1/users/exists returned isExistsPhoneNumber: true — the number is already taken by another user.
limitReachedBackend responded with 400 from setPhone or resendSms/{id} — usually an SMS-send rate limit.
unknownAny other non-401 error from GET /users, POST /users/exists, setPhone, or resendSms/{id}.

Note that submitting the user's current number is not an error — the iframe replies with PRIVATE_KIT_PHONE_UPDATED and skips the API call. See PRIVATE_KIT_UPDATE_PHONE below.

Host action: show inline error next to the field. Recommended translation-key mappings: required / invalidaccount.changePhoneForm.{reason}, existaccount.changePhoneForm.tryAnotherPhone, limitReachedcommon.errors.limitReached.

PRIVATE_KIT_PHONE_CONFIRMED

Successful response to PRIVATE_KIT_CONFIRM_PHONE. The new phone is now the user's primary number on the backend, and the backend has rotated the session JWTs as part of the confirmation.

{
type: 'PRIVATE_KIT_PHONE_CONFIRMED',
payload: {
connectionId: string;
phone: string; // the freshly-confirmed number (echoed from the backend response)
token: string; // new access JWT
refreshToken: string; // new refresh JWT
}
}

Host action: store the new tokens, update the cached profile, and close the iframe or move to the next step in your UX.

PRIVATE_KIT_PHONE_CONFIRMATION_ERROR

Any non-401 negative outcome of PRIVATE_KIT_CONFIRM_PHONE.

{
type: 'PRIVATE_KIT_PHONE_CONFIRMATION_ERROR',
payload: {
connectionId: string;
reason: PHONE_CONFIRMATION_REASON; // 'required' | 'max' | 'invalid' | 'invalidCode' | 'unknown'
message?: string;
}
}

Reasons match EMAIL_CONFIRMATION_REASON one-for-one (the verification-code rule set is identical for both channels). Recommended translation-key mappings: requiredaccount.EMAIL_VERIFICATION.verificationCode.validation.required, maxaccount.EMAIL_VERIFICATION.verificationCode.validation.max, invalidaccount.EMAIL_VERIFICATION.verificationCode.validation.isNumber, invalidCodeaccount.PHONE_VERIFICATION.invalidCode.

PRIVATE_KIT_PHONE_CODE_RESENT

Successful response to PRIVATE_KIT_RESEND_PHONE_CODE. The backend has dispatched a fresh SMS to the pending number.

{
type: 'PRIVATE_KIT_PHONE_CODE_RESENT',
payload: {
connectionId: string;
}
}

PRIVATE_KIT_PASSWORD_UPDATED

Successful response to PRIVATE_KIT_UPDATE_PASSWORD. The backend accepted the new password; no verification step is needed. Existing JWTs remain valid (the backend does not rotate them on password change).

{
type: 'PRIVATE_KIT_PASSWORD_UPDATED',
payload: {
connectionId: string;
}
}

Host action: clear the form / close the iframe / show a "password changed" toast.

PRIVATE_KIT_PASSWORD_VALIDATION_ERROR

Any non-401 negative outcome of PRIVATE_KIT_UPDATE_PASSWORD — covers missing values, new-password strength violations, and a wrong current password.

{
type: 'PRIVATE_KIT_PASSWORD_VALIDATION_ERROR',
payload: {
connectionId: string;
reason: PASSWORD_VALIDATION_REASON;
// 'requiredCurrent' | 'requiredNew' | 'min' | 'uppercase'
// | 'special' | 'number' | 'invalidCurrent' | 'unknown'
message?: string;
}
}

Reason mapping (mirrors changePasswordFormConfig rule-by-rule):

reasonMeaning
requiredCurrentcurrentPassword is empty, or the envelope was missing currentPassword / authToken.
requiredNewnewPassword is empty.
minnewPassword is shorter than 6 characters.
uppercasenewPassword has no uppercase letter (/[A-Z]/).
specialnewPassword has no special character (/[!@#$%^&*(),.?":{}|<>-]/).
numbernewPassword has no digit (/[0-9]/).
invalidCurrentBackend responded 400 to POST /private/api/v1/users/changePasswordcurrentPassword does not match.
unknownAny other non-401 error from changePassword.

The strength rules are evaluated in order (minuppercasespecialnumber) and only the first failing rule is reported. Host action: show inline error next to the relevant field. Recommended translation-key mappings: requiredCurrent / invalidCurrentaccount.changePasswordForm.oldPassword.{required,invalid}, requiredNew / min / uppercase / special / numberaccount.CREATE_ACCOUNT.password.validation.{required,min,uppercase,special,number}.

Note: the iframe does not validate the confirmation field. Password confirmation is a UX concern that lives entirely on the host — the host should enforce confirmation === newPassword before posting UPDATE_PASSWORD, and the iframe only ever receives the two fields it needs (currentPassword, newPassword).

PRIVATE_KIT_AUTH_TOKEN_401

Any 401 from the auth-central API while processing the current action — the supplied authToken is expired, revoked, or otherwise rejected.

{
type: 'PRIVATE_KIT_AUTH_TOKEN_401',
payload: {
connectionId: string;
}
}

Host action: refresh the access token (the standard useAccountRefreshToken flow), then re-send whichever action message triggered the 401 (e.g. PRIVATE_KIT_UPDATE_USERNAME, PRIVATE_KIT_UPDATE_EMAIL, PRIVATE_KIT_CONFIRM_EMAIL, PRIVATE_KIT_RESEND_EMAIL_CODE) with the fresh authToken.

parent → iframe

PRIVATE_KIT_UPDATE_USERNAME

Submits a new username for the user identified by authToken.

{
type: 'PRIVATE_KIT_UPDATE_USERNAME',
payload: {
connectionId: string;
username: string; // raw user input, will be trimmed by the iframe
authToken: string; // private access token (Bearer)
}
}

Behaviour inside the iframe:

  1. username = username.trim(). If empty → USERNAME_VALIDATION_ERROR { reason: 'required' }.
  2. userNameValidation(username). If false → USERNAME_VALIDATION_ERROR { reason: 'invalid' }.
  3. GET /private/api/v1/users to resolve id and the user's current username. 401 → AUTH_TOKEN_401, other errors → USERNAME_VALIDATION_ERROR { reason: 'unknown' }.
  4. If username.toLowerCase() === current.toLowerCase()USERNAME_UPDATED { username } immediately, without any further API call.
  5. POST /private/api/v1/users/exists with { username: username.toLowerCase() }. 401 → AUTH_TOKEN_401. isExistsUsername === trueUSERNAME_VALIDATION_ERROR { reason: 'exist' }. Other errors → USERNAME_VALIDATION_ERROR { reason: 'unknown' }.
  6. POST /private/api/v1/users/{id}/setUsername with { username }. 401 → AUTH_TOKEN_401. Other errors → USERNAME_VALIDATION_ERROR { reason: 'unknown' }. Success → USERNAME_UPDATED.

The iframe responds with exactly one outbound message per inbound UPDATE_USERNAME (assuming the connectionId matches; otherwise it is silently dropped).

PRIVATE_KIT_UPDATE_EMAIL

Submits a new email for the user identified by authToken. The backend will then send a verification code to that address; the change is committed only after PRIVATE_KIT_CONFIRM_EMAIL succeeds.

{
type: 'PRIVATE_KIT_UPDATE_EMAIL',
payload: {
connectionId: string;
email: string;
authToken: string;
}
}

Behaviour inside the iframe:

  1. email = email.trim(). If empty → EMAIL_VALIDATION_ERROR { reason: 'required' }.
  2. emailValidation(email). If false → EMAIL_VALIDATION_ERROR { reason: 'invalid' }.
  3. GET /private/api/v1/users to resolve id and the current email. 401 → AUTH_TOKEN_401, other errors → EMAIL_VALIDATION_ERROR { reason: 'unknown' }.
  4. If email.toLowerCase() === current.toLowerCase()EMAIL_UPDATED { email } immediately, without any further API call. Used both as a guard against pointless retries and as the host-friendly way to ask for re-verification of the current address (the host can then send RESEND_EMAIL_CODE).
  5. POST /private/api/v1/users/exists with { email: email.toLowerCase() }. 401 → AUTH_TOKEN_401. isExistsEmail === trueEMAIL_VALIDATION_ERROR { reason: 'exist' }. Other errors → EMAIL_VALIDATION_ERROR { reason: 'unknown' }.
  6. POST /private/api/v1/users/{id}/setEmail with { email }. 401 → AUTH_TOKEN_401. 400EMAIL_VALIDATION_ERROR { reason: 'limitReached' }. Other errors → EMAIL_VALIDATION_ERROR { reason: 'unknown' }. Success → EMAIL_UPDATED.

PRIVATE_KIT_CONFIRM_EMAIL

Confirms the pending email change with the 6-digit code that arrived in the user's inbox.

{
type: 'PRIVATE_KIT_CONFIRM_EMAIL',
payload: {
connectionId: string;
confirmationCode: string;
authToken: string;
}
}

Behaviour inside the iframe:

  1. code = confirmationCode.trim(). If empty → EMAIL_CONFIRMATION_ERROR { reason: 'required' }.
  2. code.length > 6EMAIL_CONFIRMATION_ERROR { reason: 'max' }.
  3. Not /^\d+$/EMAIL_CONFIRMATION_ERROR { reason: 'invalid' }.
  4. GET /private/api/v1/users for id. 401 → AUTH_TOKEN_401, other errors → EMAIL_CONFIRMATION_ERROR { reason: 'unknown' }.
  5. POST /private/api/v1/verification/confirm/{id} with { confirmationCode }. 401 → AUTH_TOKEN_401. 400EMAIL_CONFIRMATION_ERROR { reason: 'invalidCode' }. Other errors → EMAIL_CONFIRMATION_ERROR { reason: 'unknown' }. Success → EMAIL_CONFIRMED { email, token, refreshToken } (the new JWTs come from the backend response).

PRIVATE_KIT_RESEND_EMAIL_CODE

Asks the backend to re-send the verification code to the pending email.

{
type: 'PRIVATE_KIT_RESEND_EMAIL_CODE',
payload: {
connectionId: string;
authToken: string;
}
}

Behaviour inside the iframe:

  1. Envelope check — if authToken is missing → EMAIL_VALIDATION_ERROR { reason: 'required' }.
  2. GET /private/api/v1/users for id. 401 → AUTH_TOKEN_401, other errors → EMAIL_VALIDATION_ERROR { reason: 'unknown' }.
  3. POST /private/api/v1/verification/resendEmail/{id}. 401 → AUTH_TOKEN_401. 400EMAIL_VALIDATION_ERROR { reason: 'limitReached' }. Other errors → EMAIL_VALIDATION_ERROR { reason: 'unknown' }. Success → EMAIL_CODE_RESENT.

PRIVATE_KIT_UPDATE_PHONE

Submits a new phone for the user identified by authToken. The backend will then send an SMS code to that number; the change is committed only after PRIVATE_KIT_CONFIRM_PHONE succeeds.

{
type: 'PRIVATE_KIT_UPDATE_PHONE',
payload: {
connectionId: string;
phoneNumber: string; // international format, e.g. "+12025550101"
authToken: string;
}
}

Behaviour inside the iframe:

  1. phoneNumber = phoneNumber.trim(). If empty → PHONE_VALIDATION_ERROR { reason: 'required' }.
  2. phoneValidation(phoneNumber). If false → PHONE_VALIDATION_ERROR { reason: 'invalid' }.
  3. GET /private/api/v1/users to resolve id and the current phone. 401 → AUTH_TOKEN_401, other errors → PHONE_VALIDATION_ERROR { reason: 'unknown' }.
  4. If phoneNumber === current.trim() (exact, no normalization) → PHONE_UPDATED { phoneNumber } immediately, without any further API call. Used both as a guard against pointless retries and as the host-friendly way to ask for re-verification of the current number (the host can then send RESEND_PHONE_CODE).
  5. POST /private/api/v1/users/exists with { phoneNumber }. 401 → AUTH_TOKEN_401. isExistsPhoneNumber === truePHONE_VALIDATION_ERROR { reason: 'exist' }. Other errors → PHONE_VALIDATION_ERROR { reason: 'unknown' }.
  6. POST /private/api/v1/users/{id}/setPhone with { phoneNumber }. 401 → AUTH_TOKEN_401. 400PHONE_VALIDATION_ERROR { reason: 'limitReached' }. Other errors → PHONE_VALIDATION_ERROR { reason: 'unknown' }. Success → PHONE_UPDATED.

PRIVATE_KIT_CONFIRM_PHONE

Confirms the pending phone change with the 6-digit code that arrived via SMS.

{
type: 'PRIVATE_KIT_CONFIRM_PHONE',
payload: {
connectionId: string;
confirmationCode: string;
authToken: string;
}
}

Behaviour inside the iframe is identical to CONFIRM_EMAIL, with EMAIL_* events replaced by PHONE_*:

  1. code = confirmationCode.trim(). Empty → PHONE_CONFIRMATION_ERROR { reason: 'required' }.
  2. code.length > 6PHONE_CONFIRMATION_ERROR { reason: 'max' }.
  3. Not /^\d+$/PHONE_CONFIRMATION_ERROR { reason: 'invalid' }.
  4. GET /private/api/v1/users for id. 401 → AUTH_TOKEN_401, other errors → PHONE_CONFIRMATION_ERROR { reason: 'unknown' }.
  5. POST /private/api/v1/verification/confirm/{id} with { confirmationCode }. 401 → AUTH_TOKEN_401. 400PHONE_CONFIRMATION_ERROR { reason: 'invalidCode' }. Other errors → PHONE_CONFIRMATION_ERROR { reason: 'unknown' }. Success → PHONE_CONFIRMED { phone, token, refreshToken } (the new JWTs come from the backend response).

PRIVATE_KIT_RESEND_PHONE_CODE

Asks the backend to re-send the SMS code to the pending number.

{
type: 'PRIVATE_KIT_RESEND_PHONE_CODE',
payload: {
connectionId: string;
authToken: string;
}
}

Behaviour inside the iframe:

  1. Envelope check — if authToken is missing → PHONE_VALIDATION_ERROR { reason: 'required' }.
  2. GET /private/api/v1/users for id. 401 → AUTH_TOKEN_401, other errors → PHONE_VALIDATION_ERROR { reason: 'unknown' }.
  3. POST /private/api/v1/verification/resendSms/{id}. 401 → AUTH_TOKEN_401. 400PHONE_VALIDATION_ERROR { reason: 'limitReached' }. Other errors → PHONE_VALIDATION_ERROR { reason: 'unknown' }. Success → PHONE_CODE_RESENT.

PRIVATE_KIT_UPDATE_PASSWORD

Single-step password change. The iframe runs the same strength rules as the standalone PrivateChangePasswordForm, then submits to the backend.

{
type: 'PRIVATE_KIT_UPDATE_PASSWORD',
payload: {
connectionId: string;
currentPassword: string;
newPassword: string;
authToken: string;
}
}

Behaviour inside the iframe:

  1. If authToken missing or currentPassword not a string → PASSWORD_VALIDATION_ERROR { reason: 'requiredCurrent' }.
  2. If newPassword not a string or empty → PASSWORD_VALIDATION_ERROR { reason: 'requiredNew' }.
  3. newPassword.length < 6PASSWORD_VALIDATION_ERROR { reason: 'min' }.
  4. No uppercase → ... { reason: 'uppercase' }.
  5. No special character → ... { reason: 'special' }.
  6. No digit → ... { reason: 'number' }.
  7. POST /private/api/v1/users/changePassword with { currentPassword, newPassword }. 401 → AUTH_TOKEN_401. 400PASSWORD_VALIDATION_ERROR { reason: 'invalidCurrent' }. Other errors → PASSWORD_VALIDATION_ERROR { reason: 'unknown' }. Success → PASSWORD_UPDATED.

Confirmation matching (newPassword === confirmation) is not part of the iframe contract. Hosts that show a confirmation field must enforce the match themselves before sending UPDATE_PASSWORD.