BYOAM lets your own existing auth system — your own SMS OTP, your own password check, your own SSO — vouch for a login without threshold1 ever seeing the underlying credential. You register a method once in the dashboard; from then on, a completed login through your own system can be turned into a real threshold1 session.
There are two integration patterns — a push model and a pull model. Pick whichever fits how your own backend is set up.
Fully working today. Your own backend calls threshold1 directly, after your own auth method has already succeeded. threshold1 validates the call and issues a real session — same as any other successful login.
In the dashboard, go to your project → BYOAM → Register Method. You'll choose:
acme_sms). This is what you'll send back in the method field below.On submit, a secret is generated and shown exactly once — copy it immediately. This is what you'll use to sign every request in step 3. There's no webhook URL to configure for this pattern — your backend calls threshold1, not the other way around.
Run your own existing auth method (your own SMS code, your own password check, whatever it is) however you already do it today. threshold1 is not involved in this step and never sees the credential.
Once your own check succeeds, your backend calls:
POST /api/v1/external-auth/confirm
with:
Authorization: Bearer <your API key> — same as every other /api/v1/* call.X-Threshold1-Signature: <hex HMAC-SHA256> — the raw request body, signed with the BYOAM registration's own secret from step 1. This is a separate credential from the API key — it proves you specifically hold this registration's secret, not just any valid key for the project.import { createHmac } from "crypto";
const rawBody = JSON.stringify({
externalUserId: "your_user_123",
method: "acme_sms",
verifiedAt: new Date().toISOString(),
// email is optional — see the note below
});
const signature = createHmac("sha256", BYOAM_SECRET)
.update(rawBody)
.digest("hex");
await fetch("https://<your threshold1 host>/api/v1/external-auth/confirm", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"X-Threshold1-Signature": signature,
"Content-Type": "application/json",
},
body: rawBody, // send these exact bytes — see note below
});
Sign and send the exact same bytes. The signature is computed over the raw request body. If you build the signature from one object and then re-serialize it before sending (different key order, different whitespace), the bytes won't match what you signed and the request will fail with INVALID_SIGNATURE — even though the data is identical. Sign the literal string you're about to send, not a re-serialized copy of it.
verifiedAt has a 60-second freshness window. A request is only accepted if verifiedAt is within 60 seconds of when threshold1 receives it — this isn't a general-purpose "log a past event" endpoint, it's a live vouch for something that just happened.
email is optional, with one exception: if this externalUserId has never been seen before, threshold1 needs a real email to create the new user record — pass it the first time. Once the user exists, later confirm calls for the same externalUserId don't need to repeat it.
{
"success": true,
"token": "eyJ...",
"userId": "c3588a12-...",
"externalUserId": "your_user_123",
"isNewUser": false
}
A real session, a real JWT — from threshold1's side this is a completed login, method: "byoam". Real user.registered / user.login webhooks fire, exactly like any other successful auth.
Registrable and dashboard-testable today. Not currently consulted during any real login or registration. Read that precisely before you build against it: you can register a Pattern B method from the dashboard, and the dashboard's own Test button will call your webhook to confirm it's reachable and answering with the right shape. But there is no API endpoint and no real login or registration code path that calls out to your webhook today. Registering a method and passing the dashboard test has no effect whatsoever on real authentication — no genuine end-user login will ever reach your webhook. This is a deliberate, permanent state of this feature, not a "coming soon" — do not register this believing it functions as an enforced step-up or security control, because nothing currently enforces it.
What follows describes the real registration flow and the real contract your webhook needs to satisfy for the dashboard's Test button — useful groundwork if you're building a receiver, just not something a real login currently triggers.
In the dashboard, go to your project → BYOAM → Register Method. You'll choose:
acme_sso). This identifies the method on every check.On submit, a secret is generated and shown exactly once — copy it immediately. threshold1 signs every outbound call to your webhook with this secret, so you can verify the request genuinely came from threshold1 before trusting it.
On every check, threshold1 sends your registered webhook_url:
POST <your webhook_url>
Content-Type: application/json
X-Threshold1-Signature: <hex HMAC-SHA256 of the raw body below, keyed by your registration secret>
{
"external_user_id": "your_user_123",
"method": "acme_sso"
}
Verify the signature before trusting the request — same construction as Pattern A's inbound check, just in reverse:
import { createHmac, timingSafeEqual } from "crypto";
function isFromThreshold1(rawBody, signatureHex, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signatureHex, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Respond within 5 seconds with whether external_user_id has completed your own auth method right now:
{ "verified": true }
or { "verified": false } if they haven't — that's a normal, expected answer, not an error.
Back on the BYOAM page, your Pattern B registration has a Test button. Clicking it makes threshold1 call your registered webhook right now, with a synthetic external_user_id (dashboard-test-<timestamp>) that will never match a real user. This proves connectivity, signature handling, and response shape — it does not simulate a real login, and it isn't wired into one.
A { "verified": false } result from the test is expected and is not a failure — a synthetic ID can never legitimately verify. What the test actually tells you is whether the call reached your endpoint and came back in the right shape (reachable / times out / wrong shape), nothing more.
The dashboard's Test call fails fast rather than hanging: 5 seconds, then it gives up. If your endpoint times out, is unreachable, or returns something other than a valid { "verified": boolean } body, the test reports exactly that back to you in the dashboard. This is the same underlying call a future real integration would use — but today, it only ever runs when you click Test.
externalUserId resolution works across every method, including BYOAM.