Webhooks Overview

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 enrollmentId returned from Spot when the enrollment was originally submitted.
  • enrollment.transactionItemId: The transactionItemId assigned to the enrollment during initial coverage creation. Only present when the status is ClaimReceived.
  • 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 is ClaimReceived.
  • 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 ClaimReceived and 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 ClaimReceived and there was a payout issued.
  • claim.currencyCode: The currency the refund was paid out in. Only present when the status is ClaimReceived and 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
}