Skip to content

Reference

This page covers the public @passlock/server helpers for mailbox challenges in more detail.

All mailbox challenge helpers take shared Passlock config as their second argument:

  • tenancyId - your Passlock tenancy ID
  • apiKey - your tenancy API key
  • endpoint - optional API base URL override, defaults to https://api.passlock.dev

Mailbox challenges also share a few important concepts:

  • purpose is an application-defined string. Passlock validates it as a 1-64 character string using only A-Z, a-z, 0-9, ., _, :, and -.
  • metadata must be JSON-compatible.
  • challengeId identifies the challenge.
  • secret is stored by your app and sent back during verification.
  • code is the one-time code you deliver to the user.

Use createMailboxChallenge to start a flow and obtain the generated challengeId, secret, code, and rendered message content (html and text).

import { Passlock } from "@passlock/server";
const passlock = new Passlock({ tenancyId: "myTenancyId", apiKey: "myApiKey" });
const result = await passlock.createMailboxChallenge({
email: "jdoe@example.com",
purpose: "signup",
userId: "123",
metadata: {
signupId: "signup_123",
},
invalidateOthers: true,
});
if (result.success) {
console.log(result.value.challenge.challengeId);
console.log(result.value.challenge.message.text);
} else {
console.error(result.error.message);
}
Choose your code style
  • invalidateOthers deletes other pending challenges with the same purpose and subject.
  • When userId is present, Passlock scopes invalidation by userId; otherwise it scopes by email.
  • skipRateLimit bypasses mailbox challenge rate limiting and is usually best left unset.
  • @error/ChallengeRateLimited
  • @error/Forbidden

Use getMailboxChallenge to read a pending challenge without exposing the secret or code. This is useful when you need to restore an in-progress flow or show which mailbox is being verified.

import { Passlock } from "@passlock/server";
const passlock = new Passlock({ tenancyId: "myTenancyId", apiKey: "myApiKey" });
const result = await passlock.getMailboxChallenge({
challengeId: "challenge_123",
});
if (result.success) {
console.log(result.value._tag); // "Challenge"
console.log(result.value.email);
console.log(result.value.metadata);
} else {
console.error(result.error.message);
}
Choose your code style
  • @error/NotFound
  • @error/Forbidden

Use verifyMailboxChallenge to check the code against the stored challenge. Pass challengeId, secret, and the user-supplied code.

import { Passlock } from "@passlock/server";
const passlock = new Passlock({ tenancyId: "myTenancyId", apiKey: "myApiKey" });
const result = await passlock.verifyMailboxChallenge({
challengeId: "challenge_123",
secret: "abc123def-ghi456jkl-mno789pqr",
code: "123456",
});
if (result.success) {
console.log(result.value.challenge.email);
} else {
console.error(result.error.message);
}
Choose your code style

Successful verification returns ChallengeVerified with a readable nested challenge. The response excludes the secret and one-time code, so your app should keep validating the returned purpose, email, and userId against local expectations.

  • @error/InvalidChallenge
  • @error/InvalidChallengeCode
  • @error/ChallengeExpired
  • @error/ChallengeAttemptsExceeded
  • @error/Forbidden

Use deleteMailboxChallenge when a user cancels a flow or when you want to clear pending challenge state explicitly.

import { Passlock } from "@passlock/server";
const passlock = new Passlock({ tenancyId: "myTenancyId", apiKey: "myApiKey" });
const result = await passlock.deleteMailboxChallenge({
challengeId: "challenge_123",
});
if (result.success) {
console.log(result.value._tag); // "ChallengeDeleted"
} else {
console.error(result.error.message);
}
Choose your code style

deleteMailboxChallenge resolves to { _tag: "ChallengeDeleted" }.

  • @error/Forbidden

If you are not using Node.js, the same behavior is documented in the REST API mailbox challenge reference.