Zum Inhalt springen

API

Auf dieser Seite

Signaturen

X-Webhook-Signature mit HMAC-SHA256 prüfen, Signaturen sicher vergleichen, Zeitstempelregeln durchsetzen und den Wechsel des Geheimnisses unterstützen.

Jede Zustellung wird mit HMAC-SHA256 und Ihrem Webhook-Geheimnis signiert. Prüfen Sie die Signatur, bevor Sie verarbeiten.

Header Beschreibung
X-Webhook-Signature Zusammengesetzte Signatur: t={timestamp},v1={hmac_hex}
X-Webhook-Timestamp Unix-Sekunden dieses Versuchs — derselbe Wert, den die Signatur als t führt
X-Webhook-Id Die evt_…-Kennung dieser Zustellung, für Ihr Protokoll und Ihre Deduplizierung

An der Prüfung nimmt nur der erste Header teil. Er enthält den Zeitstempel und einen oder mehrere HMAC-Werte:

X-Webhook-Signature: t=1739281200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Der HMAC wird über timestamp + "." + raw_body gebildet:

signature_payload = str(timestamp) + "." + raw_body
hmac_hex = HMAC-SHA256(webhook_secret, signature_payload)  # lowercase hex

Der Zeitstempel ist Teil des signierten Inhalts, um Wiedereinspielung zu verhindern. Weisen Sie Zustellungen ab, bei denen |now - timestamp| > 300 Sekunden gilt.

Schritte der Prüfung

  1. X-Webhook-Signature zerlegen: t (Zeitstempel) und jeden v1-Wert herauslösen
  2. Abweisen, wenn |now - t| > 300 Sekunden
  3. signature_payload = str(t) + "." + raw_body bilden
  4. HMAC-SHA256(webhook_secret, signature_payload) berechnen
  5. Das Ergebnis in Hex-Kleinbuchstaben umwandeln
  6. Mit jedem v1-Wert über einen zeitkonstanten Vergleich abgleichen — jede Übereinstimmung gilt als gültig
  7. Mit HTTP 401 abweisen, wenn keine Signatur passt

Vergleichen Sie Signaturen immer mit einer zeitkonstanten Funktion — crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hash_equals (PHP), MessageDigest.isEqual (Java), CryptographicOperations.FixedTimeEquals (.NET). Ein einfaches == gibt das Geheimnis über die Laufzeitmessung Byte für Byte preis.

Wechsel des Geheimnisses

Während eines Wechsels des Webhook-Geheimnisses sendet Paymos 24 Stunden lang doppelte Signaturen, damit Sie ohne Ausfall umstellen können. Der Header trägt dann zwei v1-Werte: t={timestamp},v1={current_hmac},v1={previous_hmac}. Eine Signatur, die zum aktuellen oder zum vorherigen Geheimnis passt, wird akzeptiert, sodass Ihre bestehende Prüfung (Schritt 6) den gesamten Wechsel über weiterläuft.

Code zur Prüfung

import { WebhookVerifier } from '@paymos/sdk';

const verifier = new WebhookVerifier(process.env.PAYMOS_WEBHOOK_SECRET);

// Keep the body as a Buffer until verification succeeds.
const event = verifier.constructEvent(signatureHeader, rawBody);
console.log(event.eventId, event.eventType, event.data);
import os

from paymos import WebhookVerifier

verifier = WebhookVerifier(os.environ["PAYMOS_WEBHOOK_SECRET"])

# Keep raw_body as bytes until verification succeeds.
event = verifier.construct_event(signature_header, raw_body)
print(event["event_id"], event["event_type"], event["data"])
<?php
use Paymos\Webhook\WebhookEvent;
use Paymos\Webhook\WebhookVerifier;

$verifier = new WebhookVerifier(getenv('PAYMOS_WEBHOOK_SECRET'));

// Keep $rawBody unchanged until verification succeeds.
$payload = $verifier->decodeVerifiedPayload($signatureHeader, $rawBody);
$event = new WebhookEvent($payload);
echo $event->id() . ' ' . $event->type();
package main

import (
	"fmt"
	"os"
	"time"

	paymos "github.com/Paymos-labs/go-sdk/v2"
)

type invoiceEventData struct {
	InvoiceID string `json:"invoice_id"`
}

func handleWebhook(signatureHeader string, rawBody []byte) error {
	verifier, err := paymos.NewWebhookVerifier(os.Getenv("PAYMOS_WEBHOOK_SECRET"), 5*time.Minute)
	if err != nil {
		return err
	}
	var event paymos.WebhookEvent[invoiceEventData]
	if err := verifier.ConstructEvent(signatureHeader, rawBody, time.Now(), &event); err != nil {
		return err
	}
	fmt.Println(event.EventID, event.EventType, event.Data.InvoiceID)
	return nil
}
using Paymos;

var verifier = new WebhookVerifier(
    Environment.GetEnvironmentVariable("PAYMOS_WEBHOOK_SECRET")!);

// Keep rawBody as ReadOnlySpan<byte> until verification succeeds.
var webhook = verifier.ConstructEvent<InvoiceEventData>(signatureHeader, rawBody);
Console.WriteLine($"{webhook.EventId} {webhook.EventType} {webhook.Data.InvoiceId}");

public sealed record InvoiceEventData(string InvoiceId);
import com.fasterxml.jackson.databind.JsonNode;
import io.paymos.WebhookEvent;
import io.paymos.WebhookVerifier;
import java.time.Instant;

WebhookVerifier verifier = new WebhookVerifier(System.getenv("PAYMOS_WEBHOOK_SECRET"));

// Keep rawBody as byte[] until verification succeeds.
WebhookEvent<JsonNode> event =
    verifier.constructEvent(signatureHeader, rawBody, Instant.now());
System.out.println(event.eventId() + " " + event.eventType() + " " + event.data());
require 'paymos'

verifier = Paymos::WebhookVerifier.new(ENV.fetch('PAYMOS_WEBHOOK_SECRET'))

# Keep raw_body as the original String until verification succeeds.
event = verifier.construct_event(signature_header, raw_body)
puts "#{event.event_id} #{event.event_type} #{event.data}"
use paymos::{Invoice, WebhookEvent, WebhookVerifier};

let verifier = WebhookVerifier::new(std::env::var("PAYMOS_WEBHOOK_SECRET")?)?;

// Keep raw_body as &[u8] until verification succeeds.
let event: WebhookEvent<Invoice> = verifier.construct_event(signature_header, raw_body)?;
println!("{} {} {}", event.event_id, event.event_type, event.data.invoice_id);