feature:Consolidate duplicated webhook documentation to prevent drift - #198
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #158
Webhook Reference Guide
This is the canonical webhook documentation for StellarGate. For webhook delivery management endpoints (list/redeliver), see Webhook Delivery Management. For integration examples, see Integration Examples.
Overview
When a payment reaches a terminal state, StellarGate POSTs a signed JSON event to your webhook endpoint. Every request carries cryptographic headers that let you verify both authenticity and freshness — preventing replay attacks and tampering.
Event Types
StellarGate fires exactly one event when a payment settles, determined by comparing received amount to requested amount:
payment.completedFired when cumulative payment equals the requested amount exactly.
{ "event": "payment.completed", "payment_id": "a1b2c3d4-...", "merchant_id": "your-merchant-id", "tx_hash": "abc123def456...", "amount": "10.00", "paid_amount": "10.00", "asset": "XLM", "status": "completed" }payment.overpaidFired when cumulative payment exceeds the requested amount. The
deltafield shows the excess amount to consider refunding.{ "event": "payment.overpaid", "payment_id": "a1b2c3d4-...", "merchant_id": "your-merchant-id", "tx_hash": "abc123def456...", "amount": "10.00", "paid_amount": "12.50", "asset": "XLM", "status": "completed", "delta": "2.50" }payment.underpaidFired when a payment arrives but falls short of the requested amount. The
deltafield shows the remaining shortfall. The intent remains open for a top-up payment.{ "event": "payment.underpaid", "payment_id": "a1b2c3d4-...", "merchant_id": "your-merchant-id", "tx_hash": "abc123def456...", "amount": "10.00", "paid_amount": "7.00", "asset": "XLM", "status": "underpaid", "delta": "3.00" }payment.expiredFired when a payment intent's TTL elapses before payment arrives. No further transactions are watched for this intent.
{ "event": "payment.expired", "payment_id": "a1b2c3d4-...", "merchant_id": "your-merchant-id", "tx_hash": null, "amount": "10.00", "paid_amount": null, "asset": "XLM", "status": "expired" }Webhook Headers
Every webhook request includes three headers:
X-StellarGate-TimestampX-StellarGate-Signature"{timestamp}.{raw_body}"X-StellarGate-Eventeventfield from body (routing convenience)Important:
X-StellarGate-Eventis not covered by the HMAC signature. It mirrors the body'seventfield but can be altered in transit. Always read the event type from the signed JSON body after verifying the signature.Verifying Webhooks
Use this recipe to verify each incoming webhook:
Extract headers:
X-StellarGate-Timestampast(Unix seconds)X-StellarGate-Signatureassig(hex string)Check timestamp freshness:
abs(now - t) > toleranceRecompute signature:
HMAC_SHA256(WEBHOOK_SECRET, "{t}.{raw_body}"){"event":"payment.completed"...}andtis1719072645, compute HMAC over the string"1719072645.{\"event\":\"payment.completed\"...}"Constant-time comparison:
sigusing a timing-safe equality checkParse and route:
eventfield from the body to determine the event typeevent(not on theX-StellarGate-Eventheader)Verification Examples
Node.js:
Python:
Webhook Delivery Management
StellarGate tracks all webhook delivery attempts in the
webhook_deliveriestable. Two endpoints expose this history:GET /payments/:id/webhooks
List all delivery attempts for a payment.
Response (200 OK):
{ "payment_id": "550e8400-e29b-41d4-a716-446655440000", "deliveries": [ { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "url": "https://merchant.example.com/webhook", "event": "payment.completed", "status": "delivered", "attempts": 2, "last_attempt": "2026-06-22T15:30:45", "created_at": "2026-06-22T15:20:00" } ] }Error (404 Not Found):
{ "error": "payment not found" }POST /payments/:id/webhooks/:delivery_id/redeliver
Manually re-attempt a webhook delivery.
Response (200 OK):
Error (502 Bad Gateway):
{ "error": "webhook delivery failed" }Error (404 Not Found):
{ "error": "delivery not found" }Behavior:
deliveredonly if recipient returns 2xxConfiguration
Configure webhook behavior via environment variables:
WEBHOOK_SECRETWEBHOOK_RETRY_ATTEMPTS3WEBHOOK_RETRY_DELAY_MS5000WEBHOOK_TIMEOUT_SECS10WEBHOOK_REDRIVE_INTERVAL_SECS30WEBHOOK_REDRIVE_CONCURRENCY4WEBHOOK_REDRIVE_MAX_ATTEMPTS8WEBHOOK_REDRIVE_GRACE_SECS60WEBHOOK_ALLOW_PRIVATE_TARGETSfalseDelivery Guarantee
StellarGate guarantees at-least-once delivery with automatic retries:
payment_idas a deduplication key on your end. If you receive the samepayment_idtwice, it's a retry — process it idempotentlycreated_at(initial creation time) orupdated_at(last change time) from payment status to order events, not webhook delivery timesSSRF Protection
All webhook URLs are validated for SSRF attacks:
127.0.0.0/8), link-local (169.254.0.0/16), private (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), and reserved rangesWEBHOOK_ALLOW_PRIVATE_TARGETS=trueonly for local developmentIntegration Checklist
WEBHOOK_SECRETsecurely in environment (never commit)X-StellarGate-Eventheaderpayment_id)Links
POST /paymentsin README.md for payment creation withwebhook_urlIntegration Examples
Complete Workflow
Step 1: Create a payment with webhook
Step 2: User sends payment via Stellar wallet
Sends exactly 100 XLM to the destination address with memo included.
Step 3: StellarGate detects and verifies transaction
Detects on-chain transaction within ~1-10 seconds (depends on network).
Step 4: Webhook delivered to your endpoint
Step 5: Check delivery status (optional)
Response shows all attempts and their status (delivered/failed/pending).
Step 6: If delivery failed, manually redeliver
Handling Overpayment
User sends 120 XLM instead of 100 XLM:
{ "event": "payment.overpaid", "payment_id": "550e8400-...", "merchant_id": "my-shop", "tx_hash": "abc123def456...", "amount": "100.00", "paid_amount": "120.00", "asset": "XLM", "status": "completed", "delta": "20.00" }Your app should track the
deltaand issue a refund to the sender for the excess.Handling Underpayment (Top-up)
User sends 70 XLM (shortfall of 30 XLM):
{ "event": "payment.underpaid", "payment_id": "550e8400-...", "merchant_id": "my-shop", "tx_hash": "abc123def456...", "amount": "100.00", "paid_amount": "70.00", "asset": "XLM", "status": "underpaid", "delta": "30.00" }The payment intent stays open and watchable. If user sends the remaining 30 XLM (or more) to the same address and memo, you'll receive:
{ "event": "payment.completed", "payment_id": "550e8400-...", "merchant_id": "my-shop", "tx_hash": "def456abc123...", // Different on-chain transaction "amount": "100.00", "paid_amount": "100.00", // Cumulative total "asset": "XLM", "status": "completed" }Handling Expiry
No payment arrives before TTL (default 1 hour):
{ "event": "payment.expired", "payment_id": "550e8400-...", "merchant_id": "my-shop", "tx_hash": null, "amount": "100.00", "paid_amount": null, "asset": "XLM", "status": "expired" }Payment intent is no longer watched. The user must create a new payment intent if they want to retry.