Webhooks
Use Salsa's REST APIs to test and manage your Webhook endpoints.
Webhooks are used by Salsa to notify you of changes in your account. In this guide, you'll learn how to manage Webhook endpoints, verify messages, and ensure that your endpoints can receive events from our system.
Create a Webhook endpoint
To create a Webhook endpoint, send a POST
request to webhook-endpoints
. The endpointUrl
is where Salsa will send events when changes are made in your account.
curl --request POST \
--url https://api.sandbox.salsa.dev/api/rest/v1/webhook-endpoints \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'authorization: Bearer [your-auth-token]' \
--data '
{
"description": "This is my webhook endpoint that will receive events",
"endpointUrl": "https://my-webhook-url.com/path",
}
'
Example response
{
"data": {
"id": "prtnrendpt_8ae05235-927e-449f-a40a-9611bc4794ee",
"description": "This is my webhook endpoint that will receive events",
"endpointUrl": "https://my-webhook-url.com/path",
"status": "ENABLED",
"webhookVersion": "2023-01-01",
"signatureSecret": "some-secret"
}
}
By default, when a Webhook is created, status is set as Enabled
. This means that events will be sent to this Webhook endpoint. For more information, see Create WebhookEndpoint.
Update a Webhook endpoint
If you need to update an endpointURL
, description
, or status
, send a PUT
request to webhook-endpoints
. If you disable the endpoint, Salsa will no longer send Webhook events to the endpoint.
curl --request PUT \
--url https://api.sandbox.salsa.dev/api/rest/v1/webhook-endpoints \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'authorization: Bearer [your-auth-token]' \
--data '
{
"description": "This is my updated webhook endpoint that will receive events",
"endpointUrl": "https://my-webhook-url.com/new/path",
"status": "ENABLED",
"webhookVersion": "2022-08-22"
}
'
Example response
{
"data": {
"id": "prtnrendpt_8ae05235-927e-449f-a40a-9611bc4794ee",
"description": "This is my updated webhook endpoint that will receive events",
"endpointUrl": "https://my-webhook-url.com/new/pathpath",
"status": "ENABLED",
"webhookVersion": "2023-01-01"
}
}
Secure your Webhoook
Each Webhook call includes three headers that include information used for verification:
Webhook-Id
: A unique identifier for the message. Note: If a Webhook event is resent due to failure, the same ID is used.Webhook-Timestamp
: A timestamp in seconds since epoch.Webhook-Signature
: A space delimited Base64 encoded list of signatures.
Sign content
To sign content, concatenate the Webhook ID, timestamp, and payload body using .
. The signature is sensitive to any changes. This means that you should not change the body in any way before verifying.
Here's an example:
signed_content = "${webhook_id}.${webhook_timestamp}.${body}"
Calculate the expected signature
Salsa uses HMAC with SHA-256 to sign its Webhooks. To create a signature, you'll need the secret key returned as signatureSecret
in the Create Webhook Endpoint response body.
Secret key rotation
Contact support if you need to rotate the secret.
To calculate the expected signature, HMAC the signed_content
from the base64 portion of your signing secret (this is everything after whsec_
). For example, from the secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
, you'll only need MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
. The signature generated should match one of the signatures provided by the Webhook-Signature
header.
The Webhook-Signature
header is composed of a list of space delimited signatures and their corresponding version identifiers. For example:
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=
Make sure to remove the version prefix and delimiter (for example, v1,
) before verifying the signature.
Prevent attacks
Please note that to compare the signatures it's recommended to use a constant-time string comparison method in order to prevent timing attacks.
This is an example written in JavaScript, that uses actual header and body values.
const webhook_id = "msg_p5jXN8AQM9LWM0D4loKWxJek"
const webhook_timestamp = "1614265330"
const webhook_signatures = "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=".split(" ") // in case of secret rotation, the header can contain more than one signature separated by whitespace
const body = `{\"test\": 2432232314}`
console.log("Signatures: ", webhook_signatures)
const endpoint_secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
const base64_secret = "MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
const secret = Buffer.from(base64_secret, 'base64');
const signed_content = `${webhook_id}.${webhook_timestamp}.${body}`
let hmac = crypto.createHmac('sha256', secret).update(signed_content).digest('base64');
console.log("Verifying with: ", hmac);
<?php
$webhook_id = "msg_p5jXN8AQM9LWM0D4loKWxJek";
$webhook_timestamp = "1614265330";
$webhook_signatures = explode(' ', "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="); // in case of secret rotation, the header can contain more than one signature separated by whitespace
$body = '{"test": 2432232314}';
echo "Signatures: $webhook_signatures\n";
$endpoint_secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
$base64_secret = "MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
$secret = base64_decode($base64_secret);
$signed_content = "$webhook_id.$webhook_timestamp.$body";
$hmac = hash_hmac('sha256', $signed_content, $secret, true);
$hmac_base64 = base64_encode($hmac);
echo "Verifying with: $hmac_base64\n";
?>
val webhookId = "msg_p5jXN8AQM9LWM0D4loKWxJek"
val webhookTimestamp = "1614265330"
val webhookSignatures = "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=".split(" ") // in case of secret rotation, the header can contain more than one signature separated by whitespace
val body = "{\"test\": 2432232314}"
println("Signatures: $webhookSignatures")
// val endpointSecret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
val base64Secret = "MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
val secret = Base64.getDecoder().decode(base64Secret)
val signedContent = "$webhookId.$webhookTimestamp.$body"
val hash = Mac.getInstance("HmacSHA256")
.apply { init(SecretKeySpec(secret, "HmacSHA256")) }
.doFinal(signedContent.toByteArray())
val hmac = Base64.getEncoder().encodeToString(hash)
println("Verifying with: $hmac")
Verify the timestamp
Salsa also sends the timestamp of the attempt in the Webhook-Timestamp
header. Compare this timestamp against your system timestamp to ensure it's within your tolerance to prevent attacks. We recommend a window of 10 minutes.
Test an endpoint
The easiest way to test your Webhook endpoint, and ensure it's configured correctly is to trigger a few events, such as:
- Creating or updating an Employer
- Creating or updating a Worker
You'll receive a Webhook event for every Employer/Worker created or edited.
Sample Webhook event
{
"data":
{
"type": "Event.name",
"attributes":
{
... # event specific schema
}
}
}
{
"data":
{
"type": "Employer.created",
"attributes":
{
"employerId": "er_123",
"externalId": "abc",
}
}
}
Webhook events
The following actions trigger Webhooks events:
- When an Employer is created or updated
- When a Worker is created or updated
- When onboarding status for an Employer or Worker changes
- When an Employer is reminded to run payroll
- When Payroll Run is awaiting input
- When Payroll Run is confirmed
- When a worker is paid
Webhooks behave the same in the Sandbox environment as they do in production.
Notification.workerPayday
Notification.workerPayday
is not be sent in the Sandbox environment for workers who are paid via Direct Deposit. This event can be tested in the Sandbox environment by choosing to pay a worker with the Paper Check method.
Retry Schedule
Salsa attempts to deliver each Webhook message based on a retry schedule with exponential backoff. This schedule is used following a failure:
- Immediately
- 5 seconds
- 5 minutes
- 30 minutes
- 2 hours
- 5 hours
- 10 hours
- 10 hours (in addition to the previous)
For example, an attempt that fails three times before eventually succeeding will be delivered roughly 35 minutes and 5 seconds following the first attempt.
If an endpoint is disabled, delivery attempts to the endpoint will be disabled.
Updated 4 months ago