# Webhooks Receive real-time HTTP notifications when membership events occur. Appstle Memberships webhooks are powered by [Svix](https://www.svix.com/) — enterprise-grade webhook infrastructure with automatic retries, signature verification, and delivery monitoring. ## Getting Started 1. In your Appstle Memberships admin, go to **Settings → Webhooks** 2. Click **Add Endpoint** and enter your HTTPS endpoint URL 3. Select which events you want to receive (or subscribe to all) 4. Save — your endpoint will start receiving events immediately > **ℹ️ Requires Webhook Access:** Webhooks are available on paid plans. Contact [support@appstle.com](mailto:support@appstle.com) to enable. ## How It Works Webhooks are **HTTP POST requests** sent to your endpoint whenever a membership event occurs. Your endpoint must: - Be publicly accessible via HTTPS - Return a `2xx` status code within the timeout window - Process events asynchronously (queue for background processing) **Powered by Svix:** - ✅ Automatic retries with exponential backoff - ✅ Cryptographic signature verification - ✅ Detailed delivery logs and replay - ✅ Developer dashboard for monitoring ## Event Types | Event Type | Description | | --- | --- | | `membership.created` | New membership contract created | | `membership.updated` | Membership details modified | | `membership.activated` | Membership activated | | `membership.paused` | Membership paused | | `membership.cancelled` | Membership cancelled | | `membership.expired` | Membership expired at end of term | | `membership.swap-product` | Product/plan in membership changed | | `membership.next-order-date-changed` | Next renewal date rescheduled | | `membership.billing-interval-changed` | Billing frequency changed | | `membership.billing-success` | Renewal payment processed successfully | | `membership.billing-failure` | Renewal payment failed | ## Payload Structure All webhooks follow this standard structure: ```json { "type": "membership.created", "data": { // Event-specific payload } } ``` ### Membership Contract Events Events `membership.created`, `membership.updated`, `membership.activated`, `membership.paused`, `membership.cancelled`, `membership.expired`, `membership.swap-product`, `membership.next-order-date-changed`, and `membership.billing-interval-changed` all send a full membership contract payload. details summary strong 📄 Membership Contract Payload Example ```json { "type": "membership.created", "data": { "id": "gid://shopify/SubscriptionContract/12345", "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z", "nextBillingDate": "2026-02-15", "status": "ACTIVE", "billingPolicy": { "interval": "MONTH", "intervalCount": 1, "anchors": [], "maxCycles": 12, "minCycles": 1 }, "deliveryPolicy": { "interval": "MONTH", "intervalCount": 1, "anchors": [] }, "lines": { "nodes": [ { "id": "gid://shopify/SubscriptionLine/67890", "productId": "gid://shopify/Product/11111", "variantId": "gid://shopify/ProductVariant/22222", "sellingPlanId": "gid://shopify/SellingPlan/33333", "sellingPlanName": "Gold Member — Monthly", "title": "Gold Membership", "variantTitle": "Monthly", "quantity": 1, "currentPrice": { "amount": "29.00", "currencyCode": "USD" } } ] }, "customer": { "id": "gid://shopify/Customer/55555", "email": "member@example.com", "displayName": "Jane Doe", "firstName": "Jane", "lastName": "Doe", "phone": "+1-555-123-4567" }, "originOrder": { "id": "gid://shopify/Order/77777", "name": "#1001" }, "deliveryPrice": { "amount": "0.00", "currencyCode": "USD" }, "lastPaymentStatus": "SUCCEEDED", "note": null, "customAttributes": [] } } ``` ### Billing Events `membership.billing-success` and `membership.billing-failure` send billing attempt data. details summary strong ✅ membership.billing-success Payload ```json { "type": "membership.billing-success", "data": { "id": 98765, "shop": "example-store.myshopify.com", "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99999", "contractId": 12345, "status": "SUCCESS", "billingDate": "2026-02-15T00:00:00Z", "attemptTime": "2026-02-15T10:30:00Z", "attemptCount": 1, "orderId": 77778, "orderName": "#1002", "orderAmount": 29.00, "retryingNeeded": false, "billingAttemptResponseMessage": null } } ``` details summary strong ❌ membership.billing-failure Payload ```json { "type": "membership.billing-failure", "data": { "id": 98766, "shop": "example-store.myshopify.com", "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99998", "contractId": 12345, "status": "FAILURE", "billingDate": "2026-03-15T00:00:00Z", "attemptTime": "2026-03-15T10:30:00Z", "attemptCount": 1, "orderId": null, "orderName": null, "orderAmount": null, "retryingNeeded": true, "billingAttemptResponseMessage": "INVALID_PAYMENT_METHOD: The payment method is invalid." } } ``` **Common `billingAttemptResponseMessage` values:** - `INVALID_PAYMENT_METHOD` — Payment method is expired or invalid - `INSUFFICIENT_FUNDS` — Insufficient account balance - `CARD_DECLINED` — Card declined by issuer - `AUTHENTICATION_REQUIRED` — Customer must re-authenticate payment - `EXPIRED_PAYMENT_METHOD` — Payment method has expired ## Signature Verification Every webhook request is signed by Svix. **Always verify the signature** before processing the payload. Svix includes these headers on every request: - `svix-id` — Unique message ID (use for idempotency) - `svix-timestamp` — Unix timestamp of when the message was sent - `svix-signature` — HMAC-SHA256 signature Find your **webhook signing secret** in your Appstle dashboard under **Settings → Webhooks → [your endpoint]**. ### Node.js ```javascript const { Webhook } = require('svix'); const secret = 'whsec_your_signing_secret'; app.post('/webhooks/appstle-memberships', express.raw({ type: 'application/json' }), (req, res) => { const wh = new Webhook(secret); let event; try { event = wh.verify(req.body, { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }); } catch (err) { return res.status(400).send('Webhook signature verification failed'); } switch (event.type) { case 'membership.created': // Handle new membership break; case 'membership.billing-failure': // Trigger dunning flow break; case 'membership.cancelled': // Revoke member access break; } res.status(200).send('OK'); }); ``` ### Python ```python from svix.webhooks import Webhook, WebhookVerificationError secret = "whsec_your_signing_secret" @app.route('/webhooks/appstle-memberships', methods=['POST']) def webhook(): headers = { "svix-id": request.headers.get("svix-id"), "svix-timestamp": request.headers.get("svix-timestamp"), "svix-signature": request.headers.get("svix-signature"), } try: wh = Webhook(secret) event = wh.verify(request.data, headers) except WebhookVerificationError: return "Verification failed", 400 if event["type"] == "membership.billing-failure": # trigger dunning logic pass return "OK", 200 ``` See [Svix docs](https://docs.svix.com/receiving/verifying-payloads/how) for Ruby, Go, PHP, Java, and C# examples. ## Retry Schedule If your endpoint returns a non-`2xx` status or times out, Svix retries with exponential backoff across 5 attempts over 3 days. View delivery history and manually replay events from your Appstle dashboard under **Settings → Webhooks → Message Logs**. ## Idempotency Webhooks can be delivered more than once. Use the `svix-id` header as an idempotency key to safely deduplicate events in your database. ## Local Development Use [ngrok](https://ngrok.com/) to expose your local server: ```bash ngrok http 3000 # Then add https://your-id.ngrok.io/webhooks/appstle-memberships as your endpoint ``` ## Troubleshooting | Issue | Solution | | --- | --- | | Signature verification fails | Use the raw request body (before JSON parsing). Verify you're using the correct secret from the dashboard. | | Endpoint timing out | Return `200 OK` immediately, then process async. | | Not receiving events | Confirm the webhook integration is enabled in Settings and your endpoint is publicly accessible. | | Duplicate events | Use `svix-id` header for deduplication. | **Need help?** Contact [support@appstle.com](mailto:support@appstle.com)