> ## Documentation Index
> Fetch the complete documentation index at: https://docs.topsort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using Webhooks

export const LastUpdated = ({date, lang = "en"}) => {
  const translations = {
    en: "Last updated:",
    es: "Última actualización:",
    pt: "Última atualização:",
    fr: "Dernière mise à jour:",
    de: "Zuletzt aktualisiert:"
  };
  const label = translations[lang] || translations.en;
  return <>
<style>{`
.last-updated-component {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
margin-top: 12px;
margin-bottom: 16px;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.12);
color: rgba(0, 0, 0, 0.75);
line-height: 1;
}

        .last-updated-component svg {
          flex-shrink: 0;
          vertical-align: middle;
        }

        .last-updated-component span {
          display: inline-flex !important;
          align-items: center !important;
          line-height: 1 !important;
        }

        [data-theme="dark"] .last-updated-component {
          background-color: #3a3a3a;
          border: 2px solid #888888;
          color: #ffffff;
        }

        [data-theme="dark"] .last-updated-component svg {
          stroke: #ffffff;
        }
      `}</style>
      <div className="last-updated-component">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="10" />
          <polyline points="12 6 12 12 16 14" />
        </svg>
        <span>
          <strong style={{
    fontWeight: 600
  }}>{label}</strong> 
          <time dateTime={date}>{date}</time>
        </span>
      </div>
    </>;
};

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  Webhooks provide real-time notifications for events, such as campaign creation, updates, or deletions. To receive these notifications, you must specify a publicly accessible URL capable of receiving HTTP POST requests.
</div>

## How It Works

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  When an event occurs, an HTTP POST request containing event details is sent to your specified URL. Your URL must return a 2xx HTTP status code for successful requests.
</div>

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  Webhooks are created via the Create Webhook API, where you define the triggering event using the channel field. Only one webhook is allowed per channel.
</div>

### Retries and Validation

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  If your webhook URL is unreachable, we will retry sending the request up to 5 times within 1 minute, using exponential backoff. Retries occur only for 5xx or 429 HTTP status codes.
</div>

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  To ensure the authenticity and integrity of webhook deliveries, you should validate the signature. Topsort generates a signature using your webhook secret and the event payload, including it in the X-TS-Signature-256 HTTP header.
</div>

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  You can set your webhook secret during creation; otherwise, one will be generated. Store your secret securely.
</div>

<div style={{textAlign: 'justify', marginBottom: '1.5rem'}}>
  Topsort uses HMAC hex digest (starting with sha256=) to compute the signature. You must recompute the hash on your server and compare it to the X-TS-Signature-256 header to verify the signature.
</div>

### Example Signature Verification

```python theme={null}
import hmac
import hashlib

secret = "my-webhook-secret"
request = ... # incoming request from the webhook delivery

signature = hmac.new(
    key=secret.encode(),
    msg=await request.body(),
    digestmod=hashlib.sha256,
).hexdigest()

expected_signature = "sha256=" + signature
incoming_signature = request.headers["X-TS-Signature-256"]

if not hmac.compare_digest(incoming_signature, expected_signature):
    # The signature is not valid, do not process the delivery
else:
    # The signature is valid, process the delivery
```

***

<LastUpdated date="2025-11-18" />
