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:
| reason | Meaning |
|---|---|
required | The trimmed username is empty, or the message envelope was missing username / authToken. |
invalid | Username failed userNameValidation — must be ≥ 5 characters, alphanumeric, and contain at least one letter. |
exist | POST /private/api/v1/users/exists returned isExistsUsername: true — the name is already taken. |
unknown | An 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:
| reason | Meaning |
|---|---|
required | The trimmed email is empty, or the message envelope was missing email / authToken. Also used by RESEND_EMAIL_CODE when authToken is missing. |
invalid | Email failed emailValidation — basic [email protected] regex check. |
exist | POST /private/api/v1/users/exists returned isExistsEmail: true — the address is already taken by another user. |
limitReached | Backend responded with 400 from setEmail or resendEmail/{id} — usually a verification-send rate limit. Mirror of the "limit reached" toast in the standalone flow. |
unknown | Any 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 / invalid → account.changeEmailForm.{reason}, exist → account.changeEmailForm.tryAnotherEmail, limitReached → common.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):
| reason | Meaning |
|---|---|
required | confirmationCode is empty, or the envelope was missing confirmationCode / authToken. |
max | The code is longer than 6 characters. |
invalid | The code contains non-digit characters (does not match /^\d+$/). |
invalidCode | Backend responded 400 to POST /private/api/v1/verification/confirm/{id} — the code is wrong or expired. |
unknown | Any 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: required → account.EMAIL_VERIFICATION.verificationCode.validation.required, max → account.EMAIL_VERIFICATION.verificationCode.validation.max, invalid → account.EMAIL_VERIFICATION.verificationCode.validation.isNumber, invalidCode → account.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:
| reason | Meaning |
|---|---|
required | The trimmed phoneNumber is empty, or the message envelope was missing phoneNumber / authToken. Also used by RESEND_PHONE_CODE when authToken is missing. |
invalid | phoneNumber failed phoneValidation (google-libphonenumber). Send numbers in international format with a + prefix. |
exist | POST /private/api/v1/users/exists returned isExistsPhoneNumber: true — the number is already taken by another user. |
limitReached | Backend responded with 400 from setPhone or resendSms/{id} — usually an SMS-send rate limit. |
unknown | Any 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 / invalid → account.changePhoneForm.{reason}, exist → account.changePhoneForm.tryAnotherPhone, limitReached → common.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: required → account.EMAIL_VERIFICATION.verificationCode.validation.required, max → account.EMAIL_VERIFICATION.verificationCode.validation.max, invalid → account.EMAIL_VERIFICATION.verificationCode.validation.isNumber, invalidCode → account.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):
| reason | Meaning |
|---|---|
requiredCurrent | currentPassword is empty, or the envelope was missing currentPassword / authToken. |
requiredNew | newPassword is empty. |
min | newPassword is shorter than 6 characters. |
uppercase | newPassword has no uppercase letter (/[A-Z]/). |
special | newPassword has no special character (/[!@#$%^&*(),.?":{}|<>-]/). |
number | newPassword has no digit (/[0-9]/). |
invalidCurrent | Backend responded 400 to POST /private/api/v1/users/changePassword — currentPassword does not match. |
unknown | Any other non-401 error from changePassword. |
The strength rules are evaluated in order (min → uppercase → special → number) and only the first failing rule is reported. Host action: show inline error next to the relevant field. Recommended translation-key mappings: requiredCurrent / invalidCurrent → account.changePasswordForm.oldPassword.{required,invalid}, requiredNew / min / uppercase / special / number → account.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:
username = username.trim(). If empty →USERNAME_VALIDATION_ERROR { reason: 'required' }.userNameValidation(username). If false →USERNAME_VALIDATION_ERROR { reason: 'invalid' }.GET /private/api/v1/usersto resolveidand the user's currentusername. 401 →AUTH_TOKEN_401, other errors →USERNAME_VALIDATION_ERROR { reason: 'unknown' }.- If
username.toLowerCase() === current.toLowerCase()→USERNAME_UPDATED { username }immediately, without any further API call. POST /private/api/v1/users/existswith{ username: username.toLowerCase() }. 401 →AUTH_TOKEN_401.isExistsUsername === true→USERNAME_VALIDATION_ERROR { reason: 'exist' }. Other errors →USERNAME_VALIDATION_ERROR { reason: 'unknown' }.POST /private/api/v1/users/{id}/setUsernamewith{ 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:
email = email.trim(). If empty →EMAIL_VALIDATION_ERROR { reason: 'required' }.emailValidation(email). If false →EMAIL_VALIDATION_ERROR { reason: 'invalid' }.GET /private/api/v1/usersto resolveidand the currentemail. 401 →AUTH_TOKEN_401, other errors →EMAIL_VALIDATION_ERROR { reason: 'unknown' }.- 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 sendRESEND_EMAIL_CODE). POST /private/api/v1/users/existswith{ email: email.toLowerCase() }. 401 →AUTH_TOKEN_401.isExistsEmail === true→EMAIL_VALIDATION_ERROR { reason: 'exist' }. Other errors →EMAIL_VALIDATION_ERROR { reason: 'unknown' }.POST /private/api/v1/users/{id}/setEmailwith{ email }. 401 →AUTH_TOKEN_401.400→EMAIL_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:
code = confirmationCode.trim(). If empty →EMAIL_CONFIRMATION_ERROR { reason: 'required' }.code.length > 6→EMAIL_CONFIRMATION_ERROR { reason: 'max' }.- Not
/^\d+$/→EMAIL_CONFIRMATION_ERROR { reason: 'invalid' }. GET /private/api/v1/usersforid. 401 →AUTH_TOKEN_401, other errors →EMAIL_CONFIRMATION_ERROR { reason: 'unknown' }.POST /private/api/v1/verification/confirm/{id}with{ confirmationCode }. 401 →AUTH_TOKEN_401.400→EMAIL_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:
- Envelope check — if
authTokenis missing →EMAIL_VALIDATION_ERROR { reason: 'required' }. GET /private/api/v1/usersforid. 401 →AUTH_TOKEN_401, other errors →EMAIL_VALIDATION_ERROR { reason: 'unknown' }.POST /private/api/v1/verification/resendEmail/{id}. 401 →AUTH_TOKEN_401.400→EMAIL_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:
phoneNumber = phoneNumber.trim(). If empty →PHONE_VALIDATION_ERROR { reason: 'required' }.phoneValidation(phoneNumber). If false →PHONE_VALIDATION_ERROR { reason: 'invalid' }.GET /private/api/v1/usersto resolveidand the currentphone. 401 →AUTH_TOKEN_401, other errors →PHONE_VALIDATION_ERROR { reason: 'unknown' }.- 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 sendRESEND_PHONE_CODE). POST /private/api/v1/users/existswith{ phoneNumber }. 401 →AUTH_TOKEN_401.isExistsPhoneNumber === true→PHONE_VALIDATION_ERROR { reason: 'exist' }. Other errors →PHONE_VALIDATION_ERROR { reason: 'unknown' }.POST /private/api/v1/users/{id}/setPhonewith{ phoneNumber }. 401 →AUTH_TOKEN_401.400→PHONE_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_*:
code = confirmationCode.trim(). Empty →PHONE_CONFIRMATION_ERROR { reason: 'required' }.code.length > 6→PHONE_CONFIRMATION_ERROR { reason: 'max' }.- Not
/^\d+$/→PHONE_CONFIRMATION_ERROR { reason: 'invalid' }. GET /private/api/v1/usersforid. 401 →AUTH_TOKEN_401, other errors →PHONE_CONFIRMATION_ERROR { reason: 'unknown' }.POST /private/api/v1/verification/confirm/{id}with{ confirmationCode }. 401 →AUTH_TOKEN_401.400→PHONE_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:
- Envelope check — if
authTokenis missing →PHONE_VALIDATION_ERROR { reason: 'required' }. GET /private/api/v1/usersforid. 401 →AUTH_TOKEN_401, other errors →PHONE_VALIDATION_ERROR { reason: 'unknown' }.POST /private/api/v1/verification/resendSms/{id}. 401 →AUTH_TOKEN_401.400→PHONE_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:
- If
authTokenmissing orcurrentPasswordnot a string →PASSWORD_VALIDATION_ERROR { reason: 'requiredCurrent' }. - If
newPasswordnot a string or empty →PASSWORD_VALIDATION_ERROR { reason: 'requiredNew' }. newPassword.length < 6→PASSWORD_VALIDATION_ERROR { reason: 'min' }.- No uppercase →
... { reason: 'uppercase' }. - No special character →
... { reason: 'special' }. - No digit →
... { reason: 'number' }. POST /private/api/v1/users/changePasswordwith{ currentPassword, newPassword }. 401 →AUTH_TOKEN_401.400→PASSWORD_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.