Skip to main content

Documentation Index

Fetch the complete documentation index at: https://safepay.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Safepay webhooks notify you when payments, refunds, or settlements change state. Use this guide to create webhook endpoints, verify signatures, and process retries safely.

Create a webhook endpoint

1

Choose the events you need

Subscribe to the specific event types your integration depends on, such as payment.completed or refund.completed.
2

Create the webhook

Call POST /v1/aggregators/{{aggregator_id}}/webhooks with the URL and event list.
3

Store the webhook secret

Safepay returns a webhook secret (base64-encoded). Store it in your secret manager. Base64-decode it when computing the HMAC signing key.
curl --request POST "{{base_url}}/v1/aggregators/{{aggregator_id}}/webhooks" \
  --header "X-SFPY-AGGREGATOR-SECRET-KEY: {{secret_key}}" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://api.example.com/webhooks/safepay",
    "description": "Raast webhook endpoint",
    "event_types": ["payment.created", "payment.completed", "refund.completed"],
    "enabled": true
  }'

Webhook headers

Safepay includes headers that identify the event and allow you to verify authenticity. Always read:
  • X-SFPY-SIGNATURE for the HMAC signature
  • X-SFPY-TIMESTAMP for the event timestamp
The delivery also includes headers for event ID, event type, and aggregator ID.

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.
1

Extract headers

Read X-SFPY-SIGNATURE and X-SFPY-TIMESTAMP from the incoming request.
2

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.
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.
3

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.
4

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...).
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
}
Pass the raw request body bytes to Verify as body. Only after verification succeeds should you parse the JSON and process the event.

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
Respond with 200 OK as soon as you persist the event to stop further retries.

Event catalog

EventCategoryDescription
payment.createdPaymentsA new payment request was created (initiated by customer).
payment.pending_authorizationPaymentsPayment is awaiting authorization (for example, Pay Later checks).
payment.authorizedPaymentsPayment has been authorized and funds are on hold.
payment.completedPaymentsPayment has been captured or charged successfully.
payment.settledPaymentsPayment funds have been settled to the merchant.
payment.refundedPaymentsPayment was fully refunded.
payment.refund_partialPaymentsPayment was partially refunded.
payment.rejectedPaymentsPayment was rejected before authorization.
payment.failedPaymentsPayment processing failed.
payment.reversedPaymentsPayment was reversed after completion.
payment.voidedPaymentsPayment authorization was voided.
settlement.createdSettlementsSettlement request was created.
settlement.processingSettlementsSettlement is currently processing.
settlement.completedSettlementsSettlement completed successfully.
settlement.failedSettlementsSettlement failed during processing.
settlement.on_holdSettlementsSettlement temporarily placed on hold.
settlement.reversedSettlementsSettlement was reversed.
refund.createdRefundsRefund request was created.
refund.completedRefundsRefund was successfully completed.
refund.failedRefundsRefund failed during processing.
refund.canceledRefundsRefund request was canceled.

See also