# Webhook pranešimai

> Užprenumeruokite HTTPS galinį tašką sąskaitų faktūrų ir išlaidų įvykiams, tikrinkite HMAC-SHA256 parašą ir tvarkykite pakartotinius siuntimus.

Webhook pranešimai siunčia JSON žinutę į jūsų HTTPS galinį tašką kaskart, kai jūsų versle pasikeičia sąskaita faktūra ar išlaidų dokumentas, nesvarbu, ar pokytis atėjo iš API, programėlės, periodinės sąskaitos ar apmokėjimo. Jie įtraukti į tą patį planą kaip ir API.

## Prenumerata

Prenumeratų valdymo API nėra. Programėlėje atidarykite **Nustatymai**, **Integracijos**, **Webhooks**, pridėkite savo galinio taško URL ir pažymėkite norimus įvykius. Programėlė sugeneruoja prenumeratos **pasirašymo paslaptį** su priešdėliu `whsec_`. Atskleiskite ją ir išsaugokite kartu su savo galinio taško konfigūracija.

Galinio taško taisyklės:

- Privalo būti `https://`.
- Iki 2048 simbolių.

## Įvykiai

| Metodas | Kelias | Aprašymas |
|---|---|---|
| `EVENT` | [`invoice.created`](/lt/reference/invoices/webhookInvoiceCreated/) | invoice.created |
| `EVENT` | [`invoice.updated`](/lt/reference/invoices/webhookInvoiceUpdated/) | invoice.updated |
| `EVENT` | [`invoice.deleted`](/lt/reference/invoices/webhookInvoiceDeleted/) | invoice.deleted |
| `EVENT` | [`expense.created`](/lt/reference/expenses/webhookExpenseCreated/) | expense.created |
| `EVENT` | [`expense.updated`](/lt/reference/expenses/webhookExpenseUpdated/) | expense.updated |
| `EVENT` | [`expense.deleted`](/lt/reference/expenses/webhookExpenseDeleted/) | expense.deleted |

Ką kiekvienas įvykis perduoda ir kada jis siunčiamas:

| Įvykis | `data` | Kada |
|---|---|---|
| `invoice.created` | visa sąskaita, tokios pačios formos kaip `GET /invoices/{id}` | sukuriama sąskaita, kuri nėra juodraštis |
| `invoice.updated` | visa sąskaita | bet koks pokytis, įskaitant apmokėjimo būsenos pasikeitimus ir per programėlę gautus apmokėjimus |
| `invoice.deleted` | `{ "id": "<uuid>" }` | pašalinama sąskaita, kuri nėra juodraštis |
| `expense.created` | visas išlaidų dokumentas, tokios pačios formos kaip `GET /expenses/{id}` | sukuriamas išlaidų dokumentas |
| `expense.updated` | visas išlaidų dokumentas | pasikeitė skaičiai arba failas |
| `expense.deleted` | `{ "id": "<uuid>" }` | pašalinamas išlaidų dokumentas per API arba programėlėje |

Sąskaitų juodraščiai įvykių niekada nesukelia. `invoice.created` išsiunčiamas iš karto, kai sąskaita išsaugoma, dar prieš sugeneruojant jos PDF; apdorojimo funkcijoje atlikta atsisiuntimo užklausa PDF palaukia.

## Pristatymas

Kiekvienas pristatymas yra HTTP `POST` su:

```
Content-Type: application/json
Signature: <hex HMAC-SHA256 of the raw body, keyed with your whsec_ secret>
```

Turinys:

```json
{
  "event": "invoice.updated",
  "data": {
    "id": "6f1e9d2a-0b3c-4e5f-8a9b-1c2d3e4f5a6b",
    "invoice_type": "vat_invoice",
    "series": "SF",
    "invoice_number": "007",
    "payment_status": "paid",
    "…": "…"
  }
}
```

Jūsų galinis taškas privalo atsakyti bet kokia `2xx` būsena per 10 sekundžių. Viskas kita, įskaitant laiko limito viršijimą, laikoma nesėkme.

## Pakartotiniai siuntimai

Nepavykęs pristatymas kartojamas dar du kartus: po maždaug 10 sekundžių, tada po maždaug 100 sekundžių. Po trijų nesėkmių pranešimas nebesiunčiamas. Todėl pristatymai gali atkeliauti daugiau nei vieną kartą, o intensyviai keičiamo resurso atveju – ir ne iš eilės. Padarykite savo apdorojimo funkciją idempotentišką: dublikatams aptikti naudokite `data.id` kartu su `data.updated_at`, o kai svarbi eilės tvarka, gaukite dabartinę būseną per `GET /invoices/{id}`.

Įvykio identifikatoriaus ar laiko žymos antraštės nėra, programėlėje nėra ir pristatymų žurnalo.

## Patikrinkite parašą

Apskaičiuokite HMAC-SHA256 nuo **neapdoroto užklausos turinio** tiksliai tokio, koks gautas, naudodami prenumeratos paslaptį, ir palyginkite jį pastovaus laiko palyginimu su antrašte `Signature`.

```php tab="PHP" tab-group="signature"
$secret    = getenv('FSASKAITA_WEBHOOK_SECRET');   // whsec_…
$payload   = file_get_contents('php://input');
$expected  = hash_hmac('sha256', $payload, $secret);
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);
// handle $event['event'] and $event['data']
http_response_code(200);
```

```js tab="JavaScript" tab-group="signature"
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, signatureHeader, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}
```

```go tab="Go" tab-group="signature"
package webhooks

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

func Verify(rawBody []byte, signatureHeader, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
```

```python tab="Python" tab-group="signature"
import hashlib
import hmac


def verify(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), (signature_header or "").encode())
```

```java tab="Java" tab-group="signature"
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class WebhookSignature {
    public static boolean verify(byte[] rawBody, String signatureHeader, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        String expected = HexFormat.of().formatHex(mac.doFinal(rawBody));
        return MessageDigest.isEqual(
            expected.getBytes(StandardCharsets.UTF_8),
            (signatureHeader == null ? "" : signatureHeader).getBytes(StandardCharsets.UTF_8));
    }
}
```

```csharp tab="C#" tab-group="signature"
using System.Security.Cryptography;
using System.Text;

public static class WebhookSignature
{
    public static bool Verify(byte[] rawBody, string? signatureHeader, string secret)
    {
        var hash = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), rawBody);
        var expected = Convert.ToHexString(hash).ToLowerInvariant();
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(signatureHeader ?? ""));
    }
}
```

Nuskaitykite turinį kaip neapdorotus baitus prieš bet kokį JSON parsinimą; iš naujo serializuojant išparsintą objektą gali pasikeisti tarpai ar raktų tvarka, ir palyginimas nepavyks.

## Atsakykite greitai

Pirmiausia patvirtinkite gavimą `2xx` atsakymu, o apdorokite vėliau. Lėtos apdorojimo funkcijos viršija 10 sekundžių limitą, o tai sukelia pakartotinius siuntimus ir dvigubą darbą.

## Pakeiskite paslaptį

Ištrinkite prenumeratą ir sukurkite naują; nauja prenumerata gauna naują paslaptį. Jei negalite sau leisti pertrūkio, atnaujinkite galinio taško konfigūraciją prieš ištrindami senąją prenumeratą.
