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

> ## Agent Instructions
> The base URL is https://api.sudomock.com.
> Authenticate every request with the x-api-key header. Keys begin with sm_.
> A render returns the finished image at data.print_files[0].export_path. A request sent with is_async true returns a job_id to poll at GET /api/v1/jobs/{job_id}.
> Prefer the official SDKs over hand-written HTTP calls: npm install sudomock for Node, pip install sudomock for Python.

# Verifying signatures

> Check the HMAC before you act on a webhook delivery.

Every delivery is signed with the secret that belongs to the endpoint it was
sent to, so your handler can tell a real delivery from a forged request.

To get that secret from the panel:

1. Open [Dashboard, Webhooks](https://sudomock.com/dashboard/webhooks) and
   register the endpoint you want called.
2. Copy the `whsec_` value from the dialog that confirms it.

It is shown in full once, at creation and again on each
[rotation](/docs/api-reference/webhook-endpoints/rotate-the-signing-secret). Store it
before you close the dialog, then deploy the handler that reads it. If you no
longer hold it, rotate the endpoint and keep what that call returns.

## How to verify

<Tip>
  Verify against the raw request body, not a parsed object that you serialise
  again. Re-serialising changes key order and whitespace, and the signature no
  longer matches, so read the body as text before any JSON middleware touches
  it.
</Tip>

<ParamField header="X-SudoMock-Signature" type="string" required>
  `HMAC-SHA256(secret, "{timestamp}.{rawBody}")`, hex encoded.
</ParamField>

<ParamField header="X-SudoMock-Timestamp" type="string" required>
  Unix seconds at the moment the delivery was signed.
</ParamField>

<CodeGroup>
  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"vesper"}}
  import crypto from 'crypto'

  // Use the RAW request body (for example express.raw),
  // not parsed JSON.
  function verifySudoMockWebhook(req, secret) {
    const signature = req.header('X-SudoMock-Signature')
    const timestamp = req.header('X-SudoMock-Timestamp')
    const rawBody = req.body.toString('utf8')

    // Reject replays older than 5 minutes.
    const age = Math.floor(Date.now() / 1000) - Number(timestamp)
    if (!timestamp || Math.abs(age) > 300) return false

    const expected = crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${rawBody}`)
      .digest('hex')

    return crypto.timingSafeEqual(
      Buffer.from(signature || '', 'hex'),
      Buffer.from(expected, 'hex')
    )
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vesper"}}
  import hmac, hashlib, time

  def verify_sudomock_webhook(
      headers,
      raw_body: bytes,
      secret: str,
  ) -> bool:
      signature = headers.get("X-SudoMock-Signature", "")
      timestamp = headers.get("X-SudoMock-Timestamp", "")

      # Reject replays older than 5 minutes.
      try:
          age = int(time.time()) - int(timestamp)
      except (TypeError, ValueError):
          return False
      if abs(age) > 300:
          return False

      signed_payload = f"{timestamp}.".encode() + raw_body
      expected = hmac.new(
          secret.encode(),
          signed_payload,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```
</CodeGroup>

Then [send a test event](/docs/api-reference/webhook-endpoints/send-a-test-event) and
confirm your handler accepts it. It travels the same signed path as a real
delivery, so a signature that verifies there verifies in production. Once it
checks out, [Webhooks](/docs/webhooks/overview) describes the body you can trust.

## Why verify

Anyone who learns your endpoint URL can post a body to it that looks like ours.
The signature is what separates the two, because it can only be produced by a
party holding that endpoint's secret.

A genuine delivery can also be captured and sent again later. The timestamp is
what closes that door: it is covered by the signature, so refusing anything
older than a few minutes costs an attacker the whole replay.

Compare in constant time. A comparison that returns on the first differing byte
tells a caller how much of the expected value a guess got right.

A request that fails any of these checks is not from us. Return `400` and leave
the body alone.

<CardGroup cols={2}>
  <Card title="Rotate the signing secret" icon="rotate" href="/docs/api-reference/webhook-endpoints/rotate-the-signing-secret">
    Issue a new secret and read it once.
  </Card>

  <Card title="Send a test event" icon="paper-plane" href="/docs/api-reference/webhook-endpoints/send-a-test-event">
    Exercise your handler on the real signed path.
  </Card>
</CardGroup>
