# Вебхуки

Источник: https://dev.smartpdf.ru/docs/webhooks

## Как включить

Передайте в запросе операции `webhook_url` — https-адрес вашего сервера. Когда операция завершится, SmartPDF отправит на него `POST` с JSON:

```json
{
  "event": "task.done",
  "task_id": "3f2c5b0e-8a41-4d7e-9c1f-5d0b8f2e7a64",
  "operation_type": "compress",
  "status": "done",
  "timestamp": "2026-10-05T11:00:00.512341+00:00",
  "download_url": "https://api.smartpdf.ru/v1/tasks/3f2c5b0e-…/file?token=…",
  "filename": "document_smartpdf.pdf"
}
```

У неудачной операции — `"event": "task.failed"` и `error_message` вместо ссылки. Ссылка в вебхуке действует час.

## Доставка

Ответьте кодом 2xx. Иначе будет ещё три попытки — через 10, 30 и 90 секунд. Редиректы не выполняются.

Заголовки:

- `X-SmartPDF-Event` — событие, как в теле;
- `X-SmartPDF-Delivery` — id операции, одинаковый у повторов: по нему отсекайте дубли;
- `X-SmartPDF-Signature: t=<unix-время>,v1=<подпись>`.

## Проверка подписи

Секрет — в личном кабинете, в разделе [«API и MCP»](https://smartpdf.ru/dashboard/api-keys); там же выпускается новый, старый сразу перестаёт действовать. Подпись — HMAC-SHA256 от строки `<t>.<тело запроса как есть>` в hex. Сравнивайте в постоянном времени и отклоняйте запросы старше 5 минут. Проверяйте сырое тело запроса — до разбора JSON.

### cURL

```bash
# verify <секрет> <файл с телом запроса> <заголовок X-SmartPDF-Signature> — нужны openssl и date
verify() {
  local secret=$1 body=$2 header=$3 t v1 now expected
  t=$(printf "%s" "$header" | tr "," "\n" | sed -n "s/^t=//p")
  v1=$(printf "%s" "$header" | tr "," "\n" | sed -n "s/^v1=//p")
  now=$(date +%s)
  [ -n "$t" ] && [ $((now - t)) -le 300 ] && [ $((t - now)) -le 300 ] || return 1
  expected=$({ printf "%s." "$t"; cat "$body"; } | openssl dgst -sha256 -hmac "$secret" -hex | sed "s/^.* //")
  [ "$expected" = "$v1" ]
}
```

### Python

```python
import hashlib
import hmac
import time


def verify(secret: str, body: bytes, header: str) -> bool:
    """body — тело запроса как есть (байты), header — X-SmartPDF-Signature."""
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    if "t" not in parts or "v1" not in parts or abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

### Node.js

```javascript
import crypto from "node:crypto";

// rawBody — тело запроса как есть (Buffer), header — X-SmartPDF-Signature.
export function verify(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(parts.t + ".").update(rawBody).digest("hex");
  return expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

### PHP

```php
// $body — тело запроса как есть: file_get_contents('php://input'); $header — X-SmartPDF-Signature.
function verify(string $secret, string $body, string $header): bool
{
    parse_str(str_replace(',', '&', $header), $parts);
    if (!isset($parts['t'], $parts['v1']) || abs(time() - (int) $parts['t']) > 300) {
        return false;
    }
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret);
    return hash_equals($expected, $parts['v1']);
}
```

### Go

```go
// body — тело запроса как есть, header — X-SmartPDF-Signature.
// import: crypto/hmac, crypto/sha256, encoding/hex, strconv, strings, time
func verify(secret string, body []byte, header string) bool {
	parts := map[string]string{}
	for _, p := range strings.Split(header, ",") {
		if k, v, ok := strings.Cut(p, "="); ok {
			parts[k] = v
		}
	}
	t, err := strconv.ParseInt(parts["t"], 10, 64)
	if err != nil || time.Now().Unix()-t > 300 || t-time.Now().Unix() > 300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(parts["t"] + "."))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(parts["v1"]))
}
```

### Java

```java
// body — тело запроса как есть, header — X-SmartPDF-Signature.
// import: javax.crypto.Mac, javax.crypto.spec.SecretKeySpec, java.nio.charset.StandardCharsets,
//         java.security.MessageDigest, java.time.Instant, java.util.HashMap, java.util.HexFormat, java.util.Map
static boolean verify(String secret, byte[] body, String header) throws Exception {
    Map<String, String> parts = new HashMap<>();
    for (String p : header.split(",")) {
        String[] kv = p.split("=", 2);
        if (kv.length == 2) parts.put(kv[0], kv[1]);
    }
    if (!parts.containsKey("t") || !parts.containsKey("v1")) return false;
    long t = Long.parseLong(parts.get("t"));
    if (Math.abs(Instant.now().getEpochSecond() - t) > 300) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    mac.update((parts.get("t") + ".").getBytes(StandardCharsets.UTF_8));
    String expected = HexFormat.of().formatHex(mac.doFinal(body));
    return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
            parts.get("v1").getBytes(StandardCharsets.UTF_8));
}
```

### C#

```csharp
// body — тело запроса как есть, header — X-SmartPDF-Signature.
// using System.Security.Cryptography; using System.Text;
static bool Verify(string secret, byte[] body, string header)
{
    var parts = header.Split(',').Select(p => p.Split('=', 2)).Where(kv => kv.Length == 2)
        .ToDictionary(kv => kv[0], kv => kv[1]);
    if (!parts.TryGetValue("t", out var t) || !long.TryParse(t, out var ts) ||
        !parts.TryGetValue("v1", out var v1) ||
        Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return false;
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexString(hmac.ComputeHash([.. Encoding.UTF8.GetBytes(t + "."), .. body])).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(v1));
}
```

Каждый пример проверен запуском: настоящая подпись принимается, подменённое тело и старая метка времени — отклоняются.
