Webhooks allow your application to receive real-time HTTP notifications when events occur in your TRXN account. Instead of polling the API for changes, you register a URL and TRXN sends a POST request to that URL whenever a relevant event fires.
How webhooks work
Create an endpoint
Register a webhook endpoint URL and choose which events to subscribe to. You can subscribe to specific events or use ["*"] to receive all events.See the webhook endpoints API reference for CRUD operations. Store the signing secret
When you create an endpoint, TRXN returns a signing secret (format: whsec_<64_hex_chars>). Store this securely — it is only shown in full once, at creation time.
Receive events
When a subscribed event occurs, TRXN sends an HTTP POST request to your endpoint with a JSON payload describing the event.
Verify and process
Your server verifies the request signature, returns a 2xx response, then processes the event asynchronously.
Every webhook delivery sends a JSON body with this structure:
{
"event": "invoice.created",
"payload": {
"id": "inv_xxx",
"object": "invoice",
"customer_id": "cus_xxx",
"status": "pending",
"total_amount": "100.00"
},
"timestamp": "2025-01-14T12:00:00Z"
}
Signature verification
TRXN signs every webhook request with HMAC-SHA256 so you can verify that the request genuinely came from TRXN.
Every webhook request includes an X-Trxn-Signature header:
X-Trxn-Signature: t=<unix_timestamp>,v1=<signature>
Example:
X-Trxn-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Verification steps
- Extract the timestamp (
t) and signature (v1) from the header
- Check that the timestamp is within 5 minutes of the current time (prevents replay attacks)
- Compute the expected signature:
HMAC-SHA256(secret, "<timestamp>.<raw_body>")
- Compare signatures using constant-time comparison
Use the full signing secret including the whsec_ prefix as the HMAC key. This matches Stripe’s implementation pattern.
Code examples
def verify_webhook(payload, signature_header, secret)
parts = signature_header.split(",").to_h { |p| p.split("=", 2) }
timestamp = parts["t"].to_i
signature = parts["v1"]
# Check timestamp is within 5 minutes
return false if (Time.now.to_i - timestamp).abs > 300
# Compute expected signature (use full secret including whsec_ prefix)
signed_payload = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
# Constant-time comparison
ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end
Available events
Customer events
| Event | Description |
|---|
customer.created | A new customer has been created |
customer.updated | A customer has been updated |
customer.deleted | A customer has been deleted |
Product events
| Event | Description |
|---|
product.created | A new product has been created |
product.updated | A product has been updated |
product.deleted | A product has been deleted |
Price events
| Event | Description |
|---|
price.created | A new price has been created |
price.updated | A price has been updated |
price.deleted | A price has been deleted |
Invoice events
| Event | Description |
|---|
invoice.created | A new invoice has been created |
invoice.updated | An invoice has been updated |
invoice.paid | An invoice has been marked as paid |
invoice.overdue | An invoice has become overdue |
Subscription events
| Event | Description |
|---|
subscription.created | A new subscription has been created |
subscription.activated | A subscription has become active |
subscription.canceled | A subscription has been canceled |
subscription.renewed | A subscription has been renewed |
Subscription phase events
| Event | Description |
|---|
subscription_phase.created | A new subscription phase has been created |
subscription_phase.updated | A subscription phase has been updated |
subscription_phase.deleted | A subscription phase has been deleted |
Payload example:
{
"event": "subscription_phase.created",
"payload": {
"id": "sub_phase_xxx",
"object": "subscription_phase",
"subscription_id": "sub_xxx",
"start_date": "2025-02-01T00:00:00Z",
"end_date": "2025-03-01T00:00:00Z",
"items": [
{
"id": 123,
"price_id": "price_xxx",
"quantity": 1,
"overridden_price_amount": null
}
],
"created_at": "2025-01-26T12:00:00Z",
"updated_at": "2025-01-26T12:00:00Z"
},
"timestamp": "2025-01-26T12:00:00Z"
}
Wallet events
| Event | Description |
|---|
wallet.created | A new wallet has been created |
Crypto address events
| Event | Description |
|---|
crypto_address.created | A new crypto address has been added to a wallet |
crypto_address.deleted | A crypto address has been deleted |
Payment claim events
| Event | Description |
|---|
payment_claim.submitted | A customer has submitted a payment claim |
payment_claim.approved | A payment claim has been approved |
payment_claim.rejected | A payment claim has been rejected |
Crypto transaction events
| Event | Description |
|---|
crypto_transaction.received | A crypto transaction has been received |
crypto_transaction.allocated | A transaction has been allocated to an invoice |
Transaction allocation events
| Event | Description |
|---|
transaction_allocation.created | A new transaction allocation has been created |
Response handling
Your endpoint must respond with a 2xx status code to indicate successful receipt. If your endpoint returns a non-2xx status code, TRXN retries with exponential backoff for up to 11 attempts.
Error handling
| Error type | Behavior |
|---|
| Connection error | Endpoint is disabled to prevent further delivery attempts |
| Timeout error | Retry with exponential backoff |
| TLS error | Retry with exponential backoff |
| Non-2xx response | Retry with exponential backoff |
Event retention
TRXN retains webhook events for 30 days.
Best practices
Return a 2xx response before any complex processing. Process the event asynchronously after responding to avoid timeouts.
- Respond quickly — return a 2xx response before any complex logic that could cause a timeout. Process the event asynchronously after responding.
- Handle duplicates — webhook endpoints may occasionally receive the same event more than once. Make your event processing idempotent by tracking the
id from the payload.
- Handle out-of-order delivery — events may not arrive in chronological order. Use the
timestamp field to determine the sequence rather than assuming arrival order.
- Always verify signatures — use the
X-Trxn-Signature header to verify all incoming webhooks. Reject any webhook that fails signature verification.
- Use HTTPS — always use HTTPS endpoints for security.
- Store secrets securely — store your signing secrets in environment variables or a secrets manager, never in source code.
- Monitor failures — check webhook events in the dashboard to monitor delivery status.
- Regenerate compromised secrets — if you suspect your signing secret has been compromised, regenerate it immediately via the API.
Managing webhook endpoints
For creating, updating, deleting, and regenerating secrets for webhook endpoints, see the webhook endpoints API reference.