Skip to main content
You assess account creation after your auth provider creates the user. Your gate reads the first answer and lets the user in. The accounts, breach and phone sources can answer after that first answer. When they change the assessment, NoResponse sends an assessment.updated webhook with the new version. If the account’s new decision is review or block, you created an account that you would not have allowed. The late-result handler in each SDK verifies that webhook and picks an action from your policy. Then it calls the admin API of your auth provider to end the user’s sessions or to ban the user. With your own login, the SDK’s log-only provider logs what it would do until you write your own provider. Your own login shows how. A late result goes through these steps:
  1. The user creates an account with Clerk, WorkOS or Auth0. The provider creates the user and the session.
  2. Your backend calls POST /v1/assessments with the provider’s user id as account_id.
  3. The first answer is partial with allow, so the user gets in.
  4. A late source settles. NoResponse sends assessment.updated at version 2 with review to your webhook endpoint.
  5. handleLateResult verifies the signature and finds revoke_sessions for review in the policy. It calls that action through the provider’s admin API.
Assess from your backend on the first request of the new session, as Pass your auth session shows. At that time the user exists in the provider, so you send its user id as account_id. Every assessment requires account_id, and the handler acts on it.

Why evidence arrives late

An account creation assessment runs at depth: "full" by default. It waits up to 3,000 ms for its first answer. A login assessment runs at depth: "quick" and waits only for the sources that answer within 500 ms. A source that misses this deadline continues to run. The first answer lists it in detail.incomplete_sources, for example { "lane": "accounts", "reason": "timed_out" }. The assessment then has status: "partial", or "pending" when no usable answer arrived in time.
  • The accounts lane looks for accounts registered to the email and username. It is the slowest lane. When it misses the deadline, the first answer lists it as timed_out.
  • The breach lane looks for the email and username in breach tables. When it misses the deadline, detail.risk.summary.email.breach is absent until the lane settles.
  • Phone lookups always run after the first answer. The first answer lists them as deferred.
A lane can settle and change the score, band, recommendation, confidence, coverage or status. Then NoResponse publishes a new detail.assessment_version and sends it to your webhook endpoint. For example, the late lane is the breach lane, and it finds the email in a hacking-forum table. Then abuse_ecosystem_member fires, and the account’s decision moves from allow to review, as in the event below. The same event follows a pending answer. A first assessment that is pending carries no account. Your fallback can let the user in provisionally. In that case, the account’s first decision arrives in this event.

The later event

The event carries the whole assessment at its new version. It is exactly what GET /v1/assessments/{assessment_id} returns. This event followed a first answer of partial, score 15 and allow, at version 1.
assessment.updated at version 2
account is the account’s one current risk. account.risk.decision is its decision for the flow of this assessment: allow, step_up, review or block. The handler reads that value. block means that the score reached the block threshold you set. While blocking is off, block never appears. detail holds the facts of this assessment alone. detail.account_id has the same value as account.id. It is the id you sent when you assessed the account creation. Here it is the Clerk user id, so the handler acts on it unchanged. The assessment object defines every field, including band, coverage and policy_basis. Reason codes lists the reasons and their weights. Webhooks covers the envelope, its four signed headers and the delivery schedule. The SDKs name the fields in the style of each language:
  • TypeScript: account.risk.decision and detail.assessmentVersion.
  • Python: account.risk.decision and detail.assessment_version.
  • Go: Account.Risk.Decision and Detail.AssessmentVersion.

What the handler does

The handler is handleLateResult in TypeScript, handle_late_result in Python and HandleLateResult in Go. It takes the raw body, the request headers, your signing secrets and a provider. Then it does these steps:
  1. It verifies the delivery before it does anything else. A delivery that fails verification raises an error before any provider call.
  2. It reads account.risk.decision. An event without account causes no action.
  3. It finds the decision in your policy and runs the action on the user.
The result holds the verified event, the action taken (none, revoke_sessions or ban) and the provider user id that the handler acted on. You can log it or open a case in your review queue. Banning is opt-in: set block or review to ban. A policy you pass replaces the default table. It does not merge with it. A decision you leave out does nothing. For example, { block: "ban" } bans on block and does nothing on allow, step_up or review. An empty policy turns every action off. An expiry event and an endpoint.verification event never cause an action. The handler never reverses an action. When a later version says allow after a review, the handler does nothing. It is your decision to restore the account. Each provider does the two actions through its own admin API. When a session ends, the provider stops issuing new tokens for it. Tokens that the provider already issued stay valid until they expire. That is 60 seconds for a Clerk session token, and the access token lifetime you set for WorkOS and Auth0.
A WorkOS ban deletes the userWith WorkOS, ban deletes the user and everything WorkOS holds for the user. You cannot undo it. If you must reverse a decision later, keep review and block on revoke_sessions. Then send the account to your own review queue.
Auth0 plans without the sessions APIAuth0 offers the endpoints that delete sessions and refresh tokens on Enterprise plans only. Other plans refuse them with 403 feature_not_enabled. The Auth0 handler then deletes each refresh token through /api/v2/device-credentials, which every plan has. It cannot end a login session there, so revoke_sessions blocks the user, and result.action is ban. To let the user back in, unblock the user in the dashboard. The Auth0 page has the details.
When the admin API of the provider refuses a call, the handler raises ProviderActionError with the provider’s status code. Answer the webhook with a 5xx, and NoResponse delivers it again on its retry schedule. A 404 is not a refusal. The user or session is already gone, so the handler counts the action as done. Thus you can safely repeat a revoke or a ban. A delivery succeeds and does not retry when the user is already deleted or banned, or never existed. An error from your user id callback, such as a failed store read, also stops the handler. TypeScript throws it, Python raises it and ResolveUserID in Go returns it. The handlers below pass it on, and your framework answers 500. NoResponse also retries that answer.

Before you act

The handler verifies the delivery. Four more rules apply to your receiver, and the handlers below follow all of them.
  • Answer the verification challenge. The handler returns an endpoint.verification event with action none. Return the challenge with the SDK’s verification response helper, as Webhooks shows.
  • Skip test events. POST /v1/webhooks/{endpoint_id}/test sends an event with "test": true. Its fixed example assessment has the detail.account_id acct_7f3a9c, which is not one of your users. The handler takes no action on it, whatever your policy says. It never calls your user id callback for it. The SDKs show the flag as event.test in TypeScript and Python, event.Test in Go and C#, event.isTest() in Java, $event->test in PHP and event.test? in Ruby. Your own code can use it to skip the event too, for example when you record versions.
  • Skip stale revisions. Delivery is at least once and unordered, so a retry of version 2 can arrive after version 3. Act only when detail.assessment_version is higher than the highest version you saw for that assessment id. Record every version you see, including the versions that called for no action. If you do not, a late review at version 2 would revoke a user whom version 3 cleared.
  • Forget an expired assessment. The last event for an assessment is an assessment.updated with status: "expired". It carries the last version served and no assessment body. The SDKs give it its own type, which is not an Assessment: AssessmentExpiredEvent in TypeScript, and AssessmentExpired data in Python and Go. The handler takes no action on it. Delete what you hold. Retries of any version end within three hours of its first attempt, and retention is at least one day. Thus a stale retry never arrives after you forget the assessment.
The handler gets the provider user id from a callback: userId in TypeScript, resolve_user_id in Python and ResolveUserID in Go. The handler calls it only when the policy names an action. The callback receives the assessment’s detail.account_id and the verified event. Return no user id to take no action. The handlers below skip stale versions in the callback. If you assessed with your own id, map it to the provider’s user id there too. In Go, ResolveUserID receives the context of the request and returns (string, error). On a failed read, return the error of the store. HandleLateResult returns it unchanged and does not call the provider, so NoResponse retries the delivery. The older UserID callback returns only a string. It still works, and the handler ignores it when you set ResolveUserID.

The versions store

versions in the handlers is your own store. It holds the highest detail.assessment_version seen for each assessment. Keep it in your database, and share it between every instance that receives webhooks. A retry can reach a different instance from the first attempt. held returns 0 for an assessment it has not seen, so you do not need to record the version of the first answer. record keeps the higher of the stored version and the given version. Thus an older version that arrives late never lowers it. forget deletes the row. In SQL:
Versions store in SQL
The receiver records a version after the handler returns. Two deliveries can overlap, for example a retry that arrives while the first attempt still calls the provider. Both pass the version comparison, and both act. Two revokes, or two bans, end in the same state. A WorkOS ban can run after another ban deleted the user. It gets a 404 for the user, counts it as done and succeeds. An older version can also act after a newer one. For example, version 2 starts a revoke, and version 3 records allow before the revoke ends. To stop this, make the comparison, the action and the record one exclusive step per assessment across all your receivers. For example, take a row lock with SELECT ... FOR UPDATE on the version row, and record the version only after the action succeeds.

The handler

Each handler is the whole receiver for your webhook endpoint. When you registered the endpoint, you got a signing key id and secret. Store them as NORESPONSE_WEBHOOK_KEY_ID and NORESPONSE_WEBHOOK_SECRET. The secrets map takes one entry per key id. During a secret rotation, add the new key id beside the old one. The TypeScript handlers are Next.js App Router route handlers at app/webhooks/noresponse/route.ts. They work unchanged in any framework that gives you a Fetch API Request. The Python handlers use FastAPI. The Go handlers use net/http. Mount them with http.HandleFunc("POST /webhooks/noresponse", noresponseWebhook). The highlighted lines set the policy: change them to choose a different action.
Set CLERK_SECRET_KEY to your Clerk secret key. Assess with the Clerk user id, user_..., as account_id. The handler acts on it unchanged.
TypeScript
A delivery has 5 seconds to get an answer. The handler makes one provider request per active session. Thus the token request in the Auth0 handlers stops after 3 seconds. NoResponse counts a delivery that runs longer as failed and retries it. The first attempt can finish and record its version before the retry arrives. Then the retry takes no action. If not, the retry acts again, as The versions store describes.

Your own login

If your platform has its own login, the handler has no provider admin API to call. Each SDK has a log-only provider for that case. It logs one line for each action the handler would take, with the user id, and it calls nothing. Each log-only provider accepts your own log function in place of the default. The quickstart creates an account with late@test.noresponse.org. When its assessment changes to review, the default policy logs a line such as this one:
Log output
Use the log-only provider while you decide what revoking and banning mean on your platform. Then replace it with your own provider.

Write your own provider

A provider is a name and two methods. Each method gets the user id from account_id. revokeSessions ends every session of the user, so the user must go through login again. ban stops all future logins of the user and ends the user’s sessions. In the samples, sessions is your session store and users is your user table.
  • Make both methods safe to repeat. Delivery is at least once, so the handler can call a method twice for the same user. A revoke of sessions that are already gone must succeed. A disable of a user who is already disabled must also succeed.
  • Fail the delivery when your store fails. Throw or return the error. The handlers above answer 500, and NoResponse delivers the event again on its retry schedule.
  • Treat a missing user as done. A user that you deleted after account creation has nothing left to revoke.
TypeScript
Pass it to the handler in place of the log-only provider: ownLogin in TypeScript, OwnLogin() in Python and ownLogin{} in Go.

Try it

Send a test event to yourself. Use your webhooks key and the whe_ endpoint id you got when you registered the endpoint. The SDKs send it with webhookEndpoints.sendTestEvent, as that page shows. The handler itself needs no NoResponse key. It answers 204 and takes no action, because the event carries "test": true.
Send a test event
To see an action happen, do these steps:
  1. Create a user in the development instance of your provider.
  2. Start a session as that user.
  3. Call the provider object directly with the user’s id.
The user can no longer refresh the session. The current token stops working when it expires.
TypeScript
workosActions and auth0Actions in TypeScript, workos_actions and auth0_actions in Python, and WorkOSActions and Auth0Actions in Go take the same credentials as the handlers above. They have the same revokeSessions and ban methods.