Skip to content

Add a Contact Support link in your app that opens a new ticket

By Jon Henshaw4 min readUpdated

Add a Contact support link in your own app that drops a signed-in customer straight into a new ticket — with no separate password and no login screen for you to build. Your backend signs a short-lived token vouching for the user; Essential Support verifies it, creates a portal session, and opens the new-ticket form already authenticated as that customer. This is the same mechanism Essential Support’s own “Contact support” menu uses to hand agents into our help desk.

How it works

When a signed-in user clicks your Support link, your backend mints a short-lived HS256 JWT and redirects the browser to your portal’s /sso endpoint with next=/new. Essential Support verifies the signature, creates the customer’s portal session, strips the token from the address bar, and lands them on the new-ticket form.

Your app  --(user clicks Support)-->  your backend signs a JWT
          <--------------------------  302 redirect to
    https://{slug}.essential.support/sso?token=<JWT>&next=/new

The endpoint

Redirect the browser to GET https://{slug}.essential.support/sso?token=<jwt>&next=<path> on your own portal host (your {slug}.essential.support subdomain or a custom domain). next is optional and defaults to /; use /new to open the new-ticket form. next must be a relative path — absolute or protocol-relative URLs are rejected by the open-redirect guard.

The token

Sign an HS256 JWT with your workspace signing secret. Its claims:

  • email (required) — the user’s email; treated as verified because your signature vouches for it.
  • iat (required) — issued-at, in seconds since the epoch.
  • exp (required) — expiry; keep it short, about 2 minutes after iat.
  • jti (required) — a unique id per token (e.g. a UUID); recorded to reject replays.
  • sub (optional, recommended) — your stable user id; used as the primary identity key, so a user’s email can change without creating a duplicate.
  • name (optional) — display name.
  • iss (optional) — your workspace slug or id; if present it must match, as defense-in-depth.
  • locale (optional) — the user’s language (e.g. es).

Rules you must follow:

  • HS256 only — alg: none and asymmetric algorithms are rejected (the token header cannot choose the algorithm).
  • Sign server-side only. Never put the signing secret in client JavaScript, a mobile bundle, or a public repo — anyone holding it can mint sessions for your users.
  • Keep exp short; the token only has to survive the redirect. Tokens older than ~2 minutes are rejected (about 60 seconds of clock skew is allowed).
  • Make jti unique per token — it is recorded and any reuse is rejected.
  • Redirect over HTTPS only.

Where to get the signing secret

As the account owner, generate your workspace signing secret at Settings → Integrations → Signing secrets — click Generate new secret. It is shown exactly once — copy it into your backend’s secret store immediately, because it is never displayed again. You can hold two secrets at once to rotate without downtime: generate the new secret, deploy your backend with it, then retire the old one with a grace period during which tokens signed by either secret still verify. You can also generate a sample token and validate it against this secret in the Sandbox.

Sign the token (Node)

import jwt from 'jsonwebtoken';
import { randomUUID } from 'node:crypto';

const SIGNING_SECRET = process.env.ESSENTIAL_SUPPORT_SECRET; // server-side only
const PORTAL_HOST = 'acme.essential.support';                // or support.yourcompany.com

// A "Contact support" link in your app points at this backend route.
app.get('/support', requireLogin, (req, res) => {
  const now = Math.floor(Date.now() / 1000);
  const token = jwt.sign(
    {
      sub: req.user.id,      // your stable user id (preferred)
      email: req.user.email,
      name: req.user.name,
      iat: now,
      exp: now + 120,        // 2 minutes
      jti: randomUUID()
    },
    SIGNING_SECRET,
    { algorithm: 'HS256' }
  );
  res.redirect(`https://${PORTAL_HOST}/sso?token=${encodeURIComponent(token)}&next=/new`);
});

In your UI, the Support control simply links to that backend route — the token is minted server-side on each click:

<a href="/support">Contact support</a>

Point next at any relative portal path: next=/new for a fresh ticket, or next=/tickets/{ref} to open an existing one. Absolute URLs are rejected.

If a token is rejected

A bad token never dead-ends the user: Essential Support redirects to the portal sign-in with ?sso_error=1, where the customer can still get in with a magic link. Common causes:

  • Invalid signature — wrong secret, or you rotated and deployed a mismatched one.
  • Expired or too old — exp has passed, or the token is older than ~2 minutes. Keep exp short and sync your server clock (~60s skew is allowed).
  • Reused token — the same jti was presented twice. Generate a fresh jti for every redirect; don’t cache tokens.

Every accept and reject is recorded in your workspace’s auth audit log with a reason, so you can tell exactly why a token was refused.

Security notes

  • Keep exp short (~2 minutes) and mint one token per user per click.
  • Use a unique jti every time — replay is blocked server-side.
  • Redirect over HTTPS; keep the signing secret server-side only.
  • Customers are not single-seated, so the same customer can be signed in on more than one device via SSO.

Under the hood this is identical to Essential Support’s own “Contact support” menu: the agent console signs a short-lived JWT against the same /sso contract and hands the agent into help.essential.support.

Was this article helpful?

Thanks for your feedback.

Source: https://help.essential.support/integrations/contact-support-link-sso/