Webhooks
Webhooks allow your platform to receive real-time notifications from Stream when key events occur—such as payment successes, failures, or cancellations. This enables you to react to events programmatically, such as updating your database, sending notifications, or triggering internal workflows.
Stream's webhook infrastructure is designed for reliability and includes features like:
- Event-specific subscriptions
- Secure payload signing
- Delivery tracking for observability
- Automatic retries using exponential backoff
Creating a Webhook
To create or manage your webhooks, go to Webhook Settings.
There you can:
- Register a new webhook endpoint
- Select which event types you want to subscribe to
- Enable, disable, or delete webhooks
Webhook Flow Logic
Understanding which webhooks are sent in different scenarios:
Single Payment Succeeds
When a single payment succeeds:
PAYMENT_SUCCEEDEDwebhook is sent (one per payment)- If the payment completes the invoice (all payments are now paid),
INVOICE_COMPLETEDwebhook is also sent
Example: Invoice with 1 payment of 1000 SAR
- Payment succeeds →
PAYMENT_SUCCEEDEDwebhook - Invoice completes →
INVOICE_COMPLETEDwebhook
Multiple Payments Succeed (Installments)
When multiple payments succeed together (e.g., installments):
PAYMENT_SUCCEEDEDwebhook is sent for each payment that succeeded (one per payment)- If all payments complete the invoice,
INVOICE_COMPLETEDwebhook is also sent
Example: Invoice with 3 payments of 1000 SAR each
- Payments 1, 2, 3 all succeed together:
PAYMENT_SUCCEEDEDwebhook for payment_1PAYMENT_SUCCEEDEDwebhook for payment_2PAYMENT_SUCCEEDEDwebhook for payment_3INVOICE_COMPLETEDwebhook
Example: Invoice with 5 payments, payments 1-3 succeed together:
PAYMENT_SUCCEEDEDwebhook for payment_1PAYMENT_SUCCEEDEDwebhook for payment_2PAYMENT_SUCCEEDEDwebhook for payment_3- No
INVOICE_COMPLETEDwebhook (invoice not yet fully paid)
Payment Fails
When a payment fails:
PAYMENT_FAILEDwebhook is sent- No invoice completion webhook is sent
Payment Refunded
When a payment is refunded:
PAYMENT_REFUNDEDwebhook is sent- Invoice status does not change (invoice remains completed)
Payment Marked as Paid
When a payment is manually marked as paid:
PAYMENT_MARKED_AS_PAIDwebhook is sent- If this completes the invoice,
INVOICE_COMPLETEDwebhook is also sent
Subscription Renewal
When a subscription cycle renews:
- New invoice created →
INVOICE_CREATEDwebhook - Payment succeeds →
PAYMENT_SUCCEEDEDwebhook - Invoice completes →
INVOICE_COMPLETEDwebhook
Note: For subscription renewals, the INVOICE_COMPLETED webhook indicates that the subscription cycle has been renewed successfully. The invoice will have a subscription_id in its payload, and you can check if it's a renewal invoice by comparing it with previous invoices for that subscription.
Event Types
Payment Events
PAYMENT_SUCCEEDED- Payment completed successfullyPAYMENT_FAILED- Payment attempt failedPAYMENT_CANCELED- Payment was canceledPAYMENT_REFUNDED- Payment was refundedPAYMENT_MARKED_AS_PAID- Payment was manually marked as paid
Invoice Events
INVOICE_CREATED- New invoice createdINVOICE_SENT- Invoice sent to consumerINVOICE_ACCEPTED- Invoice accepted by consumerINVOICE_REJECTED- Invoice rejected by consumerINVOICE_COMPLETED- Invoice completed (all payments done)INVOICE_CANCELED- Invoice canceledINVOICE_UPDATED- Invoice details updated
Subscription Events
SUBSCRIPTION_CREATED- New subscription createdSUBSCRIPTION_ACTIVATED- Subscription activatedSUBSCRIPTION_INACTIVATED- Subscription deactivatedSUBSCRIPTION_CANCELED- Subscription canceledSUBSCRIPTION_FROZEN- Subscription frozenSUBSCRIPTION_CYCLE_RENEWAL_FAILED- Subscription cycle renewal failed (useINVOICE_COMPLETEDwithsubscription_idfor successful renewals)SUBSCRIPTION_CANCEL_AT_PERIOD_END- Subscription scheduled to cancel at period endSUBSCRIPTION_FREEZE_NOW- Subscription frozen immediatelySUBSCRIPTION_UNFREEZE_NOW- Subscription unfrozen immediatelySUBSCRIPTION_UNFREEZE_FUTURE- Subscription scheduled to unfreeze in futureSUBSCRIPTION_FREEZE_CANCEL- Subscription freeze canceled
Payment Link Events
-
PAYMENT_LINK_PAY_ATTEMPT_FAILED- Payment attempt failed for a payment linkNote: This webhook is sent when a payment link payment fails. Unlike regular payment failures, no invoice, payment, or subscription entities are created on failure, so
PAYMENT_FAILEDwebhook cannot be sent. This event exists specifically to notify about payment link failures when no entities exist.
Webhook Payload
The webhook POST request will have a JSON body like:
{
"event_type": "PAYMENT_SUCCEEDED",
"entity_type": "PAYMENT",
"entity_id": "uuid",
"entity_url": "<https://stream-app-service.streampay.sa/api/v2/payments/{id}>",
"status": "SUCCEEDED",
"data": { /* payment or event-specific data */ },
"timestamp": "2025-07-15T14:41:21.705Z"
}
Webhook Payload Example
{
"data": {
"invoice": {
"id": "2df0f7e0-2634-46ab-829a-bfcb0a797d87",
"url": "https://stream-app-service.streampay.sa/api/v2/invoices/2df0f7e0-2634-46ab-829a-bfcb0a797d87"
},
"payment": {
"id": "e2182d3d-b4cf-4972-bcc0-ec6d963c066d",
"url": "https://stream-app-service.streampay.sa/api/v2/payments/e2182d3d-b4cf-4972-bcc0-ec6d963c066d"
},
"payment_link": {
"id": "6361941e-3a81-4aa5-aaa6-60d6e052883a",
"url": "https://stream-app-service.streampay.sa/api/v2/payment_links/6361941e-3a81-4aa5-aaa6-60d6e052883a"
},
"metadata": {
"customer_id": "cust_12345",
"nested_data": {
"sub_field": "sub_value",
"sub_number": 678
},
"affiliate_id": "aff_987",
"custom_field_1": "value_1",
"custom_field_2": 12345,
"campaign_source": "email_marketing"
}
},
"status": "SUCCEEDED",
"entity_id": "e2182d3d-b4cf-4972-bcc0-ec6d963c066d",
"timestamp": "2025-07-22T14:40:31.485576",
"entity_url": "https://stream-app-service.streampay.sa/api/v2/payments/e2182d3d-b4cf-4972-bcc0-ec6d963c066d",
"event_type": "PAYMENT_SUCCEEDED",
"entity_type": "PAYMENT"
}
Webhook Request Headers
Content-Type: application/jsonUser-Agent: StreamApp-Webhook/1.0X-Webhook-Event: Event type (e.g.,PAYMENT_SUCCEEDED)X-Webhook-Entity-Type: Entity type (e.g.,PAYMENT)X-Webhook-Entity-ID: Entity UUIDX-Webhook-Signature: Format:t={timestamp},v1={signature}timestampis the UNIX timestamp when the signature was generated.signatureis an HMAC-SHA256 of{timestamp}.{payload}using your webhook’ssecret_key.
X-Webhook-Timestamp: The timestamp used in the signature.
Verifying Webhook Authenticity
To verify a webhook:
- Extract the
X-Webhook-Signatureheader and split into timestamp and signature. - Compute the HMAC-SHA256 of
{timestamp}.{raw_request_body}using your webhook’ssecret_key. - Compare your computed signature to the value in the header.
Example (Python):
import hmac, hashlib
def verify_webhook_signature(secret: str, raw_body: bytes, signature_header: str):
# signature_header: "t=TIMESTAMP,v1=SIGNATURE"
parts = dict(x.split('=') for x in signature_header.split(','))
timestamp = parts['t']
signature = parts['v1']
message = f"{timestamp}.{raw_body.decode()}"
computed = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature)
Webhook Delivery & Retry Logic
-
Delivery Attempts:
Every webhook event triggers a delivery record. Each delivery attempt and its status (pending, delivered, failed, retrying) is tracked for auditing.
-
Retry Schedule:
If your endpoint does not respond with a 2xx status, Stream will retry delivery up to 5 times.
The default retry delays (in minutes) are:
- 1st retry: 5 minutes after the first failure
- 2nd retry: 30 minutes after the second failure
- 3rd retry: 2 hours (120 minutes) after the third failure
- 4th retry: 6 hours (360 minutes) after the fourth failure
- 5th retry: 12 hours (720 minutes) after the fifth failure
After the fifth attempt, if delivery is still unsuccessful, the webhook delivery is marked as failed and no further attempts will be made.
-
Auditing:
All delivery attempts, responses, and errors are logged and can be audited.