Skip to main content
POST /api/auth/sessions/trusted-mint lets a server you control mint a Pylon session after another trusted system has verified the user’s identity. It skips the password, magic-link, and OAuth flows. Stripe Checkout is the standard use case. After Stripe verifies the buyer’s email and collects payment, trusted mint can sign the buyer into the dashboard without another sign-in step.
The endpoint checks an HMAC signature, not a session. Anyone who knows PYLON_TRUSTED_SECRET can sign in as any user. Give the secret the same protection as a database password: store it only in environment variables, rotate it after any leak, and never log it.

Enabling the endpoint

The endpoint is off by default. Until you set PYLON_TRUSTED_SECRET, requests receive a plain 404. This keeps a default Pylon install from shipping a signing surface that does nothing.
The same secret must be available to whatever code signs the requests: your Next.js route handler, your Worker, or your CLI tool. Use the same secret-management approach you already use for STRIPE_SECRET_KEY. The two secrets need the same level of protection.

Wire format

Signature algorithm:
Hex output is lowercase. Timestamps are Unix seconds. A request outside ±5 minutes of the server clock returns 401 STALE_TIMESTAMP. The format matches Stripe’s webhook signature scheme, so it will look familiar if you have built a Stripe webhook handler before.

Response

On a 2xx response, the endpoint also sends the same Set-Cookie header that /api/auth/magic/verify sends. A browser-facing proxy can forward the response directly, and the user ends up signed in. Pylon creates the Session the same way it creates a magic-link session: revocable from /api/auth/sessions and listed under the user’s devices.

Failure modes

Example: Next.js Stripe Checkout success handler

Edge cases

  • Multi-tenant orgs. The minted session matches what /api/auth/magic/verify produces. No tenant is auto-selected. Apps that need a tenant should call /api/auth/select-org after the user lands (the dashboard typically does this automatically, based on a stored “last org” hint).
  • Locked or banned users. If the User row has a non-null disabledAt, bannedAt, lockedAt, or _deletedAt column, the endpoint returns 403 ACCOUNT_LOCKED and logs a sign_in_failed audit event. These are conventions, not framework-enforced columns. Apps that do not model lockouts can ignore them.
  • Email already in use under a guest cookie. Trusted-mint does not merge guest data automatically. That behavior is specific to magic-link verify, where the user explicitly proves control of the email. If you need anonymous-cart merging on Stripe success, do it explicitly in your handler after the mint succeeds.
  • Cookie pass-through to the browser. The endpoint always sends Set-Cookie, even when called server-to-server. If you do not forward the header to a browser, ignore it. The token in the response body is the same session, and it works as Authorization: Bearer pylon_….
  • Rate limiting. The standard PYLON_RATE_LIMIT_MAX per-IP anonymous bucket applies. A bot that finds the endpoint URL but does not have the secret hits the rate limit within seconds and cannot get further. The secret is the real security boundary.

Audit log

Every call writes one of three event shapes to the audit log: The intent field is the free-form string from the request body. Use it to tell apart “Stripe checkout success,” “internal SSO bridge,” and “BYOC migration backfill” without parsing audit reasons.

Replay window

The signature scheme defends against two things. First, tampering: an attacker cannot change the email in a captured request without invalidating the signature. Second, stale replay: a signature stops working after 5 minutes. It does not defend against a fresh replay, a captured request resent within that same window. Anyone who can reach the Pylon ingress and replays a captured signed request within 5 minutes mints another session for the same email. The response body includes a long-lived token. This creates two risks:
  • A TLS man-in-the-middle in the request path (a corporate proxy, or a CDN that logs full bodies) can extract a usable session token within the 5-minute window.
  • An attacker who steals one signed request from server logs that capture the URL and headers can replay it, once per minute, for up to 5 minutes. This is rare, since signatures live in headers and bodies are not usually logged.
Mitigations:
  • Do not log signed requests together with their headers.
  • Use HTTPS end-to-end. The HMAC signature does not replace TLS.
  • If your threat model needs single-use requests, add a nonce to the request body and enforce uniqueness in your own backend before calling trusted-mint. The trusted-mint endpoint does not de-duplicate requests on its own; Pylon’s audit log shows every replay.
Pylon accepts this trade-off. A per-request nonce store would add complexity and a new failure mode: if the nonce store is unavailable, the endpoint fails. For Stripe Checkout style use cases, the 5-minute window works well, because the user is actively in the flow at that moment.

Security checklist

  • PYLON_TRUSTED_SECRET is 32+ bytes of CSPRNG output (openssl rand -hex 32).
  • Set the secret only on machines that need to sign or verify requests. A sign-only frontend does not need to know it.
  • Restrict the endpoint to your trusted server-side environments where possible. Most production Pylon deployments are public, so the secret is the only barrier against attackers.
  • Keep server clocks within a few seconds of NTP. The ±5 minute window is not as wide as it looks: a 7-minute clock skew makes the endpoint stop working.
  • To rotate the secret: set a new PYLON_TRUSTED_SECRET, redeploy the signing service, wait about 10 minutes for in-flight signed requests to flush, then update the Pylon side. Pylon accepts only one active secret today; multi-key rotation is planned.
  • Send the audit log somewhere queryable (Tinybird, Loki, Datadog), so you can alert on a sign_in_failed spike for the trusted_mint method. Every post-signature rejection (invalid JSON, missing or invalid email, USER_NOT_FOUND, INVALID_USER_ROW, USER_INSERT_FAILED, ACCOUNT_LOCKED) emits one, with the matching meta.reason.
  • Confirm the TLS terminator (Cloudflare, the Fly proxy, or similar) does not log signed-request payloads. Bodies are sensitive, even though headers usually are not.

HMAC requests vs. service tokens

A long-lived service bearer token could work for the same use case, but it has three drawbacks:
  • Service tokens leak. Once leaked, the attacker can sign in as anyone, indefinitely. This is the same blast radius as PYLON_TRUSTED_SECRET, except the secret never travels with the request, so observability tools that log headers do not capture it by accident.
  • Service tokens do not bind to the request body. An attacker who captures one signed request can replay it. The HMAC signature plus timestamp stops that.
  • Token rotation needs a coordinated cutover on both sides. Secret rotation is a single environment-variable change on each side; you do not deploy a new long-lived credential.
Use pk.* API keys when the trusted server needs to act as one specific user. Use trusted-mint when the trusted server needs to sign in as any user. These are deliberately different threat models.