Webhooks allow Spot to send partners real-time status updates for enrollments. Enrollments are processed asynchronously, so these callbacks ensure your system always reflects the current state. You are responsible for hosting and maintaining your webhook endpoint.
Spot provides the following endpoints for managing your webhook configuration:
Webhook Payload
Spot will invoke the webhook any time an enrollment's status changes. For Refund Guarantee (CFAR) enrollments, the webhook can emit a status of ClaimReceived, which is the most important status to listen for and handle.
An example payload is below:
{
"timestamp": "2025-11-21T21:33:08.933Z",
"enrollment": {
"id": "123456-9e7b-47e1-aaeb-ghjfkhjl",
"transactionItemId": "abc-123",
"status": "ClaimReceived",
"resolvedByPartner": true,
"startDate": "2026-01-17T23:00:00.000Z",
"endDate": "2026-01-18T23:00:00.000Z"
},
"offer": {
"sku": "123456-CFAR0101C0V"
},
"claim": {
"amount": 200,
"percent": 0.9,
"currencyCode": "USD"
}
}Field descriptions:
- timestamp: An ISO8601 date-time indicating when the enrollment most recently transitioned status.
- enrollment.id: The
enrollmentIdreturned from Spot when the enrollment was originally submitted. - enrollment.transactionItemId: The
transactionItemIdassigned to the enrollment during initial coverage creation. Only present when the status isClaimReceived. - enrollment.status: The most recent status of the enrollment. Webhooks will only emit for the following status values:
Enrolled: The enrollment has been processed successfully.Failed: Enrollment failed to process. This is rare - Spot monitors and retries automatically where possible. In some cases, partner coordination may be required to resolve underlying data issues.ClaimReceived(CFAR only): The customer has activated their benefit and will not attend. Partners may use this signal to release inventory.
- enrollment.resolvedByPartner: If
true, the webhook was triggered by a partner-initiated resolve or cancel call. Only present when the status isClaimReceived. - enrollment.startDate: The start date of Spot coverage. Only present if the status is not
Failed. - enrollment.endDate: The end date of Spot coverage. Only present if the status is not
Failed. - offer.sku: The offer SKU associated with this enrollment.
- claim.amount: The refund amount in dollars the customer received from Spot. Only present when the status is
ClaimReceivedand there was a payout issued. - claim.percent: The % refund that was paid out to the customer, in decimals (ie. 0.5 = 50% refund, 1 = 100% refund). Only present when the status is
ClaimReceivedand there was a payout issued. - claim.currencyCode: The currency the refund was paid out in. Only present when the status is
ClaimReceivedand there was a payout issued.
Authenticating Webhook Payloads
Spot signs every webhook using an HMAC (Hash-based Message Authentication Code). We use HMAC-SHA256, and the resulting 64-character hex digest is sent in the X-Spot-Signature header of each webhook request.
To verify a payload, stringify the request body and compute an HMAC-SHA256 digest using your partner ID and shared HMAC secret (both found in the Partner Portal).
In theory, since the HMAC secret is known only to you and Spot, you can be certain that the original payload came from Spot and was not modified in transit. This is done by comparing your calculated message digest to the message digest included in the X-Spot-Signature header. If your calculated value matches the digest from the request header, then the payload is valid and the request can be processed. If the header is missing, or your calculated digest does not match, you can halt any further processing.
Below are examples of generating and verifying the digest in NodeJS as well as PHP - Spot can provide guidance on how to do so in other languages as well:
NodeJS
import { createHmac, timingSafeEqual } from 'crypto';
const SPOT_SIGNATURE_HEADER = 'X-Spot-Signature';
const HASH_ALGORITHM = 'sha256';
const ENCODING = 'hex';
const HMAC_SECRET = process.env.SPOT_WEBHOOK_SHARED_SECRET;
const SPOT_PARTNER_ID = process.env.SPOT_PARTNER_ID;
// assumes an Express webserver route
app.post('/my/spot/webhook', (req, res) => {
// concatenate the partner id and HMAC secret, separated by a colon
const hmacKey = `${SPOT_PARTNER_ID}:${HMAC_SECRET}`;
// create the digest as a string
const hmac = createHmac(HASH_ALGORITHM, hmacKey);
const digest = hmac.update(JSON.stringify(req.rawBody)).digest(ENCODING);
// extract the provided signature from the request headers
const expectedSignature = req.get(SPOT_SIGNATURE_HEADER) || '';
// convert to Buffer objects for use with an equals comparison that is not vulnerable to timing attacks
const digestBuffer = Buffer.from(digest, 'utf8');
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
// compare locally computed digest versus the expected digest from the header
if (digestBuffer.length === expectedBuffer.length && timingSafeEqual(digestBuffer, expectedBuffer)) {
// authenticated; process the payload as necessary here
res.json({ message: "OK" });
} else {
// invalid; do not process this payload
res.status(401).send('Not authorized');
}
};PHP
$spotSignatureHeader = 'X-Spot-Signature';
$hashAlgorithm = 'sha256';
$hmacSecret = getEnv('SPOT_WEBHOOK_SHARED_SECRET');
$spotPartnerId = getEnv('SPOT_PARTNER_ID');
// concatenate the partner id and HMAC secret, separated by a colon
$hmacKey = "{$spotPartnerId}:{$hmacSecret}";
// stringify the request body
$requestBody = json_encode($_POST);
// create the digest
$digest = hash_hmac($hashAlgorithm, $requestBody, $hmacKey);
// extract the provided signature from the request headers
$expectedSignature = $_SERVER['HTTP_X_SPOT_SIGNATURE'];
// compare the locally computed digest versus the expected digest from the header
if (hash_equals($digest, $expectedSignature)) {
echo "Valid signature";
// authenticated; process the payload as necessary here
} else {
echo "Invalid signature.";
// invalid; do not process this payload
}