Letterbook
All docs

Letterbook Docs

Authenticated ticket form submissions

Link logged-in users to the portal ticket form with signed customer identity.

Last updated June 26, 2026

What this guide covers#

Authenticated ticket submissions let your app send a logged-in customer to the Letterbook user portal with a signed user ID. The customer still fills out the normal portal form, but Letterbook can verify that the request came from your product and associate the ticket with that authenticated user.

Use this when customers start from an authenticated area of your app, such as a help menu, billing page, account settings page, or in-product support link.

Setup overview#

  1. Confirm the user portal ticket form is enabled. It is enabled by default.
  2. Confirm your portal URL. Your default Letterbook portal host is already set up, and you can also use a custom domain such as help.company.com.
  3. Generate a portal ticket auth secret from the Ticket Identity section in Letterbook settings.
  4. Store the secret in your app's server-side secret manager.
  5. Create signed ticket form links for logged-in users from server-side code.
  6. Send authenticated customers to /submit-ticket with user_id, user_id_timestamp, and user_id_signature.
  7. Submit a test ticket and confirm the ticket is associated with the expected authenticated user.

The portal ticket auth secret is shown only when generated or rotated. Store it immediately. When rotated, the previous secret is accepted for a short grace period so you can update your app without breaking active links.

Generate the signature on your server, not in browser code. The signing secret must never be exposed to customers.

The signed link should point to your portal's /submit-ticket page and include:

  • user_id: the authenticated user's stable ID in your system.
  • user_id_timestamp: the current Unix timestamp in seconds.
  • user_id_signature: the HMAC signature your server creates.

The simplest flow is:

  1. Your frontend asks your backend for a signed portal URL.
  2. Your backend signs the current user's ID with your portal ticket auth secret.
  3. Your frontend sends the customer directly to the signed portal URL.

Frontend#

async function openSupport() {
  const response = await fetch("/api/letterbook/support-url", {
    method: "POST",
  });
  const { url } = await response.json();

  window.location.href = url;
}

Backend#

import crypto from "node:crypto";

export async function POST() {
  const currentUser = await getCurrentUser();
  const portalBaseUrl = "https://help.company.com";
  const portalHost = "help.company.com";
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const message = `${portalHost}:${currentUser.id}:${timestamp}`;
  const signature = crypto
    .createHmac("sha256", process.env.LETTERBOOK_PORTAL_TICKET_SECRET!)
    .update(message)
    .digest("hex");

  const url = new URL("/submit-ticket", portalBaseUrl);
  url.searchParams.set("user_id", currentUser.id);
  url.searchParams.set("user_id_timestamp", timestamp);
  url.searchParams.set("user_id_signature", signature);

  return Response.json({ url: url.toString() });
}

Use the portal hostname customers visit for portalHost, without the scheme, path, or port. For example, use help.company.com, not https://help.company.com/submit-ticket.

The signature message is:

portal_host:user_id:user_id_timestamp

Verification rules#

Signed links are short-lived. The timestamp must be within 5 minutes of the current time, so generate links when the customer opens support. If a customer opens the ticket form without signed identity params, the portal submits a normal public ticket.

Testing checklist#

Before launch, test:

  • A signed link from a logged-in user creates a ticket.
  • The ticket is associated with the expected authenticated user ID.
  • An expired timestamp is rejected.
  • A tampered user ID or signature is rejected.