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

# Webhooks

> Receive asynchronous updates when Raast payments or payouts change state.

Raast is asynchronous: customers approve or reject requests inside their banking apps, and Safepay pushes the resulting status to your backend via webhooks. Subscribe to these notifications to drive real-time UI updates and back-office automations.

## Webhook flow

<Steps>
  <Step title="Create a webhook endpoint">
    Use `POST /v1/aggregators/{{aggregator_id}}/webhooks` with the event types you want.
  </Step>

  <Step title="Receive the event">
    Safepay sends events with identifying headers, the event timestamp, and a signed payload.
  </Step>

  <Step title="Verify and acknowledge">
    Verify the webhook signature using the timestamp and raw request body (see below), then parse and persist the event, and respond with `200 OK`.
  </Step>
</Steps>

## Headers

Safepay includes headers for event ID, event type, aggregator ID, signature, and timestamp. Always read:

* `X-SFPY-SIGNATURE`
* `X-SFPY-TIMESTAMP`

Use the event ID header for idempotency and safe retries.

## Signature verification

Safepay computes the webhook signature over `timestamp + '.' + raw_body`: the `X-SFPY-TIMESTAMP` value, a literal period, then the raw HTTP request body bytes. Use the signature header and timestamp header to verify authenticity **before** parsing JSON. The webhook secret returned at creation time is base64-encoded; decode it to get the HMAC key.

<Steps>
  <Step title="Extract headers">
    Read `X-SFPY-SIGNATURE` and `X-SFPY-TIMESTAMP` from the incoming request.
  </Step>

  <Step title="Build the signing payload">
    Read the raw HTTP request body bytes exactly as received. Build the signing payload as `timestamp + '.' + raw_body`. Use the `X-SFPY-TIMESTAMP` header value exactly as received, without reformatting it.

    <Callout type="warning">Do not parse, prettify, re-serialize, or otherwise modify the request body before verification. Any modification to the body bytes will cause signature verification to fail.</Callout>
  </Step>

  <Step title="Compute HMAC">
    Base64-decode the webhook secret and use the decoded bytes as the HMAC-SHA256 key. Compute the HMAC over the full signing payload (`timestamp + '.' + raw_body`), not the raw body alone.
  </Step>

  <Step title="Format and compare signatures">
    Format the expected signature as `sha256=` plus a lowercase hexadecimal digest, and compare it to the `X-SFPY-SIGNATURE` header using a constant-time compare (for example `sha256=abcdef...`).
  </Step>
</Steps>

```go theme={null}
func Verify(secretBase64 string, body []byte, providedSig, providedTS string, tolerance time.Duration) error {
	if tolerance > 0 {
		parsed, err := time.Parse(time.RFC3339Nano, providedTS)
		if err != nil {
			return errInvalidTimestamp
		}
		if delta := time.Since(parsed); delta > tolerance || delta < -tolerance {
			return errTimestampDrift
		}
	}

	decodedSecret, err := base64.StdEncoding.DecodeString(secretBase64)
	if err != nil {
		return errInvalidSecret
	}

	mac := hmac.New(sha256.New, decodedSecret)
	mac.Write([]byte(providedTS))
	mac.Write([]byte{'.'})
	mac.Write(body)

	expected := fmt.Sprintf("sha256=%s", hex.EncodeToString(mac.Sum(nil)))
	if !hmac.Equal([]byte(expected), []byte(providedSig)) {
		return errSignatureMismatch
	}

	return nil
}
```

<Callout type="info">Pass the raw request body bytes to `Verify` as `body`. Only after verification succeeds should you parse the JSON and process the event. See [Webhooks delivery](/guides/webhooks-delivery) for a full walkthrough.</Callout>

## Retry behavior

Safepay retries failed deliveries up to 5 attempts using exponential backoff:

* Attempt 1: 1 second
* Attempt 2: 2 seconds
* Attempt 3: 4 seconds
* Attempt 4: 8 seconds
* Attempt 5: 16 seconds

## Event catalog

| Event                           | Category    | Description                                                        |
| ------------------------------- | ----------- | ------------------------------------------------------------------ |
| `payment.created`               | Payments    | A new payment request was created (initiated by customer).         |
| `payment.pending_authorization` | Payments    | Payment is awaiting authorization (for example, Pay Later checks). |
| `payment.authorized`            | Payments    | Payment has been authorized and funds are on hold.                 |
| `payment.completed`             | Payments    | Payment has been captured or charged successfully.                 |
| `payment.settled`               | Payments    | Payment funds have been settled to the merchant.                   |
| `payment.refunded`              | Payments    | Payment was fully refunded.                                        |
| `payment.refund_partial`        | Payments    | Payment was partially refunded.                                    |
| `payment.rejected`              | Payments    | Payment was rejected before authorization.                         |
| `payment.failed`                | Payments    | Payment processing failed.                                         |
| `payment.reversed`              | Payments    | Payment was reversed after completion.                             |
| `payment.voided`                | Payments    | Payment authorization was voided.                                  |
| `settlement.created`            | Settlements | Settlement request was created.                                    |
| `settlement.processing`         | Settlements | Settlement is currently processing.                                |
| `settlement.completed`          | Settlements | Settlement completed successfully.                                 |
| `settlement.failed`             | Settlements | Settlement failed during processing.                               |
| `settlement.on_hold`            | Settlements | Settlement temporarily placed on hold.                             |
| `settlement.reversed`           | Settlements | Settlement was reversed.                                           |
| `refund.created`                | Refunds     | Refund request was created.                                        |
| `refund.completed`              | Refunds     | Refund was successfully completed.                                 |
| `refund.failed`                 | Refunds     | Refund failed during processing.                                   |
| `refund.canceled`               | Refunds     | Refund request was canceled.                                       |

## Key endpoints

| Endpoint                                                                              | Purpose                    |
| ------------------------------------------------------------------------------------- | -------------------------- |
| `POST /v1/aggregators/{{aggregator_id}}/webhooks`                                     | Create a webhook.          |
| `GET /v1/aggregators/{{aggregator_id}}/webhooks`                                      | List webhook endpoints.    |
| `GET /v1/aggregators/{{aggregator_id}}/webhooks/{{webhook_id}}`                       | Read a webhook endpoint.   |
| `PUT /v1/aggregators/{{aggregator_id}}/webhooks/{{webhook_id}}`                       | Update a webhook endpoint. |
| `DELETE /v1/aggregators/{{aggregator_id}}/webhooks/{{webhook_id}}`                    | Delete a webhook endpoint. |
| `PUT /v1/aggregators/{{aggregator_id}}/webhooks/{{webhook_id}}/rotate`                | Rotate the webhook secret. |
| `GET /v1/aggregators/{{aggregator_id}}/webhooks/{{endpoint}}/deliveries`              | List webhook deliveries.   |
| `GET /v1/aggregators/{{aggregator_id}}/webhooks/{{endpoint}}/deliveries/{{delivery}}` | Read a webhook delivery.   |

## See also

* [Webhooks delivery](/guides/webhooks-delivery)
* [Payment lifecycle guide](/guides/payment-lifecycle)
* [Refunds](/concepts/refunds)
* [Pagination](/reference/pagination)
* [API reference](/api-reference)
