Skip to content

Reference

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

All mailbox challenge helpers take these shared fields:

  • 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 { createMailboxChallenge } from "@passlock/server";
const result = await createMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
email: "jdoe@example.com",
purpose: "signup",
userId: "123",
metadata: {
signupId: "signup_123",
},
invalidateOthers: true,
});
console.log(result.challenge.challengeId);
console.log(result.challenge.secret);
console.log(result.challenge.code);
console.log(result.challenge.message.html);
  • 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 { getMailboxChallenge } from "@passlock/server";
const challenge = await getMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
challengeId: "challenge_123",
});
console.log(challenge._tag); // "Challenge"
console.log(challenge.email);
console.log(challenge.metadata);
  • @error/NotFound
  • @error/Forbidden

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

import { verifyMailboxChallenge } from "@passlock/server";
const result = await verifyMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
challengeId: "challenge_123",
secret: "abc123def-ghi456jkl-mno789pqr",
code: "123456",
});
console.log(result.challenge._tag); // "Challenge"
console.log(result.challenge.purpose);
console.log(result.challenge.email);

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 { deleteMailboxChallenge } from "@passlock/server";
await deleteMailboxChallenge({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
challengeId: "challenge_123",
});

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.