Customize authentication flows with your own logic.
Actions change the behavior of various flows within the WorkOS platform including user registration and authentication using custom logic.
When an action is configured for a particular request type, WorkOS synchronously calls the associated action endpoint and waits for a response that allows or denies the operation. When WorkOS calls an actions endpoint, the request includes contextual metadata such as the profile of the user performing the operation, the organization associated with the operation, or the IP address – all of which are available for decisioning within the endpoint.
Configure actions that execute during various user operations:
To configure actions:
Create a public endpoint that WorkOS can make requests to. This endpoint should use HTTPS and should accept POST requests with the workos-signature header. This header is used for verifying the request’s authenticity from WorkOS.
export async function POST(req: Request) { const payload = await req.json(); const sigHeader = req.headers.get('workos-signature'); // Verify the signature and process the event return new Response(null, { status: 200 }); }
WorkOS sends the header as WorkOS-Signature, but many web servers normalize HTTP request headers to their lowercase variants.
Set the actions endpoint URL in the WorkOS Dashboard. Set Enable action and choose Save changes.

Each actions endpoint must specify its error handling behavior. By default, if there is an issue reaching the endpoint or validating the response, WorkOS denies the operation. To change this behavior, select a different option depending on the action endpoint type; for example, for authentication actions, select Allow authentication.
Upon receiving a request, respond with an HTTP 200 OK as well as a valid response body to signal to WorkOS that the request was successfully handled.
Before processing the request payload, verify the request was sent by WorkOS and not an unknown party.
WorkOS includes a unique signature in each actions request, allowing verification of the request’s authenticity. To verify this signature, obtain the secret generated when setting up the actions endpoint in the WorkOS dashboard. Store this secret securely on the actions endpoint server as an environment variable.
The SDKs provide a method to validate the timestamp and signature of an actions. Examples using these methods are included below. The parameters are the payload (raw request body), the request header, and the actions secret.
import { SignatureVerificationException, WorkOS } from '@workos-inc/node'; const workos = new WorkOS(process.env.WORKOS_API_KEY); export async function POST(req: Request) { const payload = await req.json(); const sigHeader = req.headers.get('workos-signature'); if (!sigHeader) { console.error('WorkOS signature missing'); return Response.json( { error: 'WorkOS signature missing' }, { status: 401 }, ); } if (!process.env.WORKOS_ACTIONS_SECRET) { console.error('Actions secret missing'); return Response.json({ error: 'Actions secret missing' }, { status: 500 }); } let action; try { action = await workos.actions.constructAction({ payload: payload, sigHeader: sigHeader, secret: process.env.WORKOS_ACTIONS_SECRET, }); } catch (err) { if (err instanceof SignatureVerificationException) { console.error(err.message); return Response.json({ error: 'Invalid signature' }, { status: 401 }); } } // Verify the signature and process the event return new Response(null, { status: 200 }); }
There is an optional parameter, tolerance, that sets the time validation for the actions request in seconds. The SDK methods have default values for tolerance, usually 3 – 5 minutes.
To implement actions request validation manually, follow these steps:
First, extract the timestamp and signature from the header. There are two values to parse from the WorkOS-Signature header, delimited by a , character.
| Key | Value |
|---|---|
issued_timestamp | The number of milliseconds since the epoch time at which the event was issued, prefixed by t= |
signature_hash | The HMAC SHA256 hashed signature for the request, prefixed by v1= |
To avoid replay attacks, validate that the issued_timestamp does not differ too much from the current time.
Next, construct the expected signature. The expected signature is computed from the concatenation of:
issued_timestamp. characterHash the string using HMAC SHA256, using the actions secret as the key. The expected signature will be the hex digest of the hash. Finally, compare signatures to make sure the actions request is valid.
Once the event request is confirmed as validly signed, use the event payload in the application’s business logic.
WorkOS sends actions requests from a fixed set of IP addresses. Restrict access to the actions endpoint to only these IP addresses:
3.217.146.166 23.21.184.92 34.204.154.149 44.213.245.178 44.215.236.82 50.16.203.9 52.1.251.34 52.21.49.187 174.129.36.47
The endpoint must respond with a signed JSON object indicating a verdict of Allow or Deny as well as an optional error_message in the event the verdict is Deny.
Based on the payload data received, determine whether to allow or deny the operation. Each action type receives a different payload model, so handle the appropriate data in the handler.
The SDK provides a method to create the signed response.
import { SignatureVerificationException, UserRegistrationActionResponseData, WorkOS, } from '@workos-inc/node'; const workos = new WorkOS(process.env.WORKOS_API_KEY); export async function POST(req: Request) { const payload = await req.json(); const sigHeader = req.headers.get('workos-signature'); if (!sigHeader) { console.error('WorkOS signature missing'); return Response.json( { error: 'WorkOS signature missing' }, { status: 401 }, ); } if (!process.env.WORKOS_ACTIONS_SECRET) { console.error('Actions secret missing'); return Response.json({ error: 'Actions secret missing' }, { status: 500 }); } let action; try { action = await workos.actions.constructAction({ payload: payload, sigHeader: sigHeader, secret: process.env.WORKOS_ACTIONS_SECRET, }); } catch (err) { if (err instanceof SignatureVerificationException) { console.error(err.message); return Response.json({ error: 'Invalid signature' }, { status: 401 }); } } let responsePayload: UserRegistrationActionResponseData; // Determine whether to allow or deny the action if ( action?.object === 'user_registration_action_context' && action?.userData.email.split('@')[1] === 'gmail.com' ) { responsePayload = { type: 'user_registration', verdict: 'Deny', errorMessage: 'Please use a work email address', }; } else { responsePayload = { type: 'user_registration', verdict: 'Allow' }; } const response = await workos.actions.signResponse( responsePayload, process.env.WORKOS_ACTIONS_SECRET, ); return Response.json(response); }
To construct the actions response manually, follow these steps:
First, store the current epoch timestamp to a variable.
Next, construct the JSON response. The JSON response must contain the following:
timestamp: The epoch timestamp you recordedverdict: Indicates whether to allow or deny the action. Allowed values: 'Allow' | 'Deny' | 'allow' | 'deny'error_message: An optional, 500 character maximum string. This should only be provided with a verdict of deny or DenyNext, construct the signature. The expected signature is computed from the concatenation of:
. characterHash the string using HMAC SHA256, using the actions secret as the key. The expected signature will be the hex digest of the hash.
Finally, the endpoint should respond with a JSON object containing the following properties:
object: 'authentication_action_response' | 'user_registration_action_response'payload: The JSON response you formed abovesignature The hex digest of the hash you created aboveSend test actions from the dashboard after configuring an endpoint. Go to the actions Test tab and click the Send test action button.

To test against a local development environment, use a tool like ngrok to create a secure tunnel to your local machine, and send test webhooks to the public endpoint generated with ngrok. See the blog post for more details on testing webhooks locally with ngrok.