Webhooks
Webhooks let ECOSIRE push real-time notifications to your server when events occur — no polling required. Configure your webhook URL in Dashboard → Settings → Webhooks.
Available Events
| Event | Trigger |
|---|---|
checkout.completed | A Stripe checkout session completes (order + licenses created) |
invoice.paid | A Stripe invoice is paid (subscription renewal) |
subscription.deleted | A customer cancels their subscription |
subscription.updated | Subscription status changes (e.g., trial → active, active → past_due) |
charge.refunded | A charge is refunded (licenses auto-revoked) |
payment.failed | A payment attempt fails |
license.activated | A license key is activated against a domain |
license.revoked | A license is manually revoked by an admin |
Payload Format
Every webhook POST has:
- Method:
POST - Content-Type:
application/json - Headers:
X-Ecosire-Event,X-Ecosire-Signature,X-Ecosire-Delivery
Common envelope
{
"id": "evt_018e1234abcd70008000000000000001",
"event": "checkout.completed",
"createdAt": "2026-03-20T12:00:00.000Z",
"data": { ... }
}
Event Payloads
checkout.completed
{
"id": "evt_018e...",
"event": "checkout.completed",
"createdAt": "2026-03-20T12:00:00Z",
"data": {
"orderId": "018e1234-abcd-7000-8000-000000000010",
"customerId": "018e1234-abcd-7000-8000-000000000002",
"customerEmail": "[email protected]",
"amount": 49900,
"currency": "usd",
"lineItems": [
{
"productId": "018e...",
"productName": "ECOSIRE Shopify Connector",
"quantity": 1,
"unitAmount": 49900
}
],
"licenses": [
{
"licenseKey": "SHOP-XXXX-XXXX-XXXX-XXXX",
"productId": "018e...",
"activationsLimit": 3,
"expiresAt": "2027-03-20T12:00:00Z"
}
]
}
}
invoice.paid
{
"id": "evt_018e...",
"event": "invoice.paid",
"createdAt": "2026-03-20T12:00:00Z",
"data": {
"invoiceId": "inv_018e...",
"customerId": "018e...",
"customerEmail": "[email protected]",
"amount": 14900,
"currency": "usd",
"subscriptionId": "sub_1PxxxStripeId",
"periodStart": "2026-03-20T00:00:00Z",
"periodEnd": "2026-04-20T00:00:00Z"
}
}
subscription.updated
{
"id": "evt_018e...",
"event": "subscription.updated",
"createdAt": "2026-03-20T12:00:00Z",
"data": {
"subscriptionId": "sub_1PxxxStripeId",
"customerId": "018e...",
"previousStatus": "active",
"currentStatus": "past_due",
"licenses": [
{ "licenseKey": "SHOP-XXXX-XXXX-XXXX-XXXX", "action": "suspended" }
]
}
}
charge.refunded
{
"id": "evt_018e...",
"event": "charge.refunded",
"createdAt": "2026-03-20T12:00:00Z",
"data": {
"orderId": "018e...",
"chargeId": "ch_1PxxxStripeId",
"amount": 49900,
"currency": "usd",
"licenses": [
{ "licenseKey": "SHOP-XXXX-XXXX-XXXX-XXXX", "action": "revoked" }
]
}
}
license.activated
{
"id": "evt_018e...",
"event": "license.activated",
"createdAt": "2026-03-20T12:00:00Z",
"data": {
"licenseKey": "SHOP-XXXX-XXXX-XXXX-XXXX",
"domain": "mystore.myshopify.com",
"activationsCount": 2,
"activationsLimit": 3
}
}
license.revoked
{
"id": "evt_018e...",
"event": "license.revoked",
"createdAt": "2026-03-20T12:00:00Z",
"data": {
"licenseKey": "SHOP-XXXX-XXXX-XXXX-XXXX",
"revokedBy": "admin",
"reason": "Refund processed"
}
}
Signature Verification
Every webhook request includes an X-Ecosire-Signature header — a SHA256 HMAC of the raw request body signed with your webhook secret.
Always verify the signature before processing the event.
Node.js
import crypto from 'crypto';
function verifyWebhook(
rawBody: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/ecosire', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-ecosire-signature'] as string;
const secret = process.env.ECOSIRE_WEBHOOK_SECRET!;
if (!verifyWebhook(req.body.toString(), sig, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
// process event...
res.json({ received: true });
});
Python
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ECOSIRE_WEBHOOK_SECRET"].encode()
@app.route("/webhooks/ecosire", methods=["POST"])
def handle_webhook():
sig = request.headers.get("X-Ecosire-Signature", "")
expected = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json()
# process event...
return {"received": True}
PHP
$secret = $_ENV['ECOSIRE_WEBHOOK_SECRET'];
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_ECOSIRE_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($body, true);
Retry Policy
If your endpoint does not return 2xx within 10 seconds, ECOSIRE marks the delivery as failed and retries:
| Attempt | Delay |
|---|---|
| 1st retry | 5 minutes |
| 2nd retry | 30 minutes |
| 3rd retry | 2 hours |
After 3 failed retries the delivery is permanently marked as failed. You can manually replay any failed delivery from Dashboard → Settings → Webhooks → Delivery Log.
Best Practices
-
Respond immediately. Return
200 OKas soon as the signature is verified. Move heavy processing to a background queue (Bull, Celery, etc.). -
Idempotency. The same event may be delivered more than once (network retries). Use
id(the event UUID) as an idempotency key — deduplicate in your DB before processing. -
Verify every request. Never skip signature verification — it is your only guarantee the request originated from ECOSIRE.
-
Log raw payloads. Store the raw JSON body alongside the event ID for debugging and audit trails.
-
Use HTTPS. Webhook endpoints must be HTTPS in production. HTTP endpoints are rejected.
-
Separate concerns. Run one webhook handler per event type; avoid a monolithic switch/case that grows unbounded.
Testing Webhooks Locally
Use a tunneling tool to expose your local server:
# ngrok
ngrok http 3001
# Then configure the HTTPS URL in Dashboard → Settings → Webhooks
# e.g., https://a1b2c3d4.ngrok.io/webhooks/ecosire
Trigger a test delivery from the dashboard by clicking Send test event on any event type.