Skip to content
Last updated

🔔 Webhooks

Real-Time Event Notifications

Webhooks allow you to receive real-time notifications when subscription events occur in your Appstle account. Build reactive, event-driven integrations with automatic retries, signature verification, and detailed delivery logs.

Webhooks are HTTP POST requests sent to a URL you configure (your endpoint). When an event happens, Appstle sends a POST request with event data to your endpoint.

Powered by Svix — Enterprise-grade webhook infrastructure with:

  • ✅ Automatic retries with exponential backoff
  • ✅ Cryptographic signature verification
  • ✅ Detailed delivery logs and monitoring
  • ✅ Developer-friendly debugging tools

🚀 Getting Started

1️⃣ Configure Your Endpoint

  • Log in to your Appstle dashboard
  • Navigate to SettingsWebhooks
  • Add your webhook endpoint URL
  • Select which events you want to receive

2️⃣ Respond Quickly

  • Return a 2xx status code (e.g., 200 OK)
  • Process events asynchronously in background
  • Return success immediately
  • Timeouts trigger automatic retries

3️⃣ Verify Signatures

  • Always verify webhook signatures
  • Ensures requests are authentic
  • Prevents unauthorized access
  • See Signature Verification below

📡 Event Types

Appstle sends webhooks for the following subscription events:

Event TypeDescriptionPayload Type
subscription.createdNew subscription createdSubscription Contract
subscription.updatedSubscription details updatedSubscription Contract
subscription.activatedSubscription activatedSubscription Contract
subscription.pausedSubscription pausedSubscription Contract
subscription.cancelledSubscription cancelledSubscription Contract
subscription.next-order-date-changedNext order date modifiedSubscription Contract
subscription.billing-interval-changedBilling frequency changedSubscription Contract
subscription.billing-successPayment processed successfullyBilling Attempt
subscription.billing-failurePayment failedBilling Attempt
subscription.billing-skippedBilling cycle skippedBilling Attempt
subscription.upcoming-order-notificationUpcoming order reminderBilling Attempt

Webhook Payload Structure

All webhooks follow this standard structure:

{
  "type": "subscription.created",
  "data": {
    // Event-specific payload (see detailed examples below)
  }
}

Complete Webhook Payloads

Subscription Contract Events

The following events send complete subscription contract data:

  • subscription.created
  • subscription.updated
  • subscription.activated
  • subscription.paused
  • subscription.cancelled
  • subscription.next-order-date-changed
  • subscription.billing-interval-changed
📄 Complete Subscription Contract Payload Example
{
  "type": "subscription.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",
    "deliveryPrice": {
      "amount": "5.00",
      "currencyCode": "USD"
    },
    "lastPaymentStatus": "SUCCEEDED",
    "billingPolicy": {
      "interval": "MONTH",
      "intervalCount": 1,
      "anchors": [
        {
          "type": "MONTHDAY",
          "day": 15,
          "month": null,
          "cutoffDay": null
        }
      ],
      "maxCycles": null,
      "minCycles": null
    },
    "deliveryPolicy": {
      "interval": "MONTH",
      "intervalCount": 1,
      "anchors": [
        {
          "type": "MONTHDAY",
          "day": 15,
          "month": null,
          "cutoffDay": null
        }
      ]
    },
    "lines": {
      "nodes": [
        {
          "id": "gid://shopify/SubscriptionLine/67890",
          "productId": "gid://shopify/Product/11111",
          "variantId": "gid://shopify/ProductVariant/22222",
          "sellingPlanId": "gid://shopify/SellingPlan/33333",
          "sellingPlanName": "Subscribe & Save 10%",
          "title": "Premium Coffee Beans",
          "variantTitle": "1lb / Medium Roast",
          "sku": "COFFEE-1LB-MED",
          "quantity": 2,
          "taxable": true,
          "currentPrice": {
            "amount": "27.00",
            "currencyCode": "USD"
          },
          "lineDiscountedPrice": {
            "amount": "24.30",
            "currencyCode": "USD"
          },
          "variantImage": {
            "transformedSrc": "https://cdn.shopify.com/s/files/1/0001/2345/products/coffee.jpg"
          },
          "pricingPolicy": {
            "basePrice": {
              "amount": "30.00",
              "currencyCode": "USD"
            },
            "cycleDiscounts": [
              {
                "afterCycle": 0,
                "adjustmentType": "PERCENTAGE",
                "adjustmentValue": {
                  "percentage": 10.0
                },
                "computedPrice": {
                  "amount": "27.00",
                  "currencyCode": "USD"
                }
              },
              {
                "afterCycle": 3,
                "adjustmentType": "PERCENTAGE",
                "adjustmentValue": {
                  "percentage": 15.0
                },
                "computedPrice": {
                  "amount": "25.50",
                  "currencyCode": "USD"
                }
              }
            ]
          },
          "discountAllocations": [
            {
              "amount": {
                "amount": "2.70",
                "currencyCode": "USD"
              },
              "discount": {
                "id": "gid://shopify/SubscriptionManualDiscount/44444"
              }
            }
          ],
          "customAttributes": [
            {
              "key": "Roast Level",
              "value": "Medium"
            },
            {
              "key": "Grind Type",
              "value": "Whole Bean"
            }
          ]
        }
      ],
      "pageInfo": {
        "hasNextPage": false,
        "hasPreviousPage": false,
        "startCursor": "eyJsYXN0X2lkIjo2Nzg5MH0",
        "endCursor": "eyJsYXN0X2lkIjo2Nzg5MH0"
      }
    },
    "customer": {
      "id": "gid://shopify/Customer/55555",
      "email": "customer@example.com",
      "displayName": "John Doe",
      "firstName": "John",
      "lastName": "Doe",
      "phone": "+1-555-123-4567"
    },
    "customerPaymentMethod": {
      "id": "gid://shopify/CustomerPaymentMethod/66666",
      "instrument": {
        "__typename": "CustomerCreditCard",
        "brand": "VISA",
        "expiresSoon": false,
        "expiryMonth": 12,
        "expiryYear": 2028,
        "firstDigits": "424242",
        "lastDigits": "4242",
        "maskedNumber": "•••• •••• •••• 4242",
        "name": "John Doe",
        "source": "SHOPIFY",
        "isRevocable": true,
        "billingAddress": {
          "firstName": "John",
          "lastName": "Doe",
          "address1": "123 Main Street",
          "city": "San Francisco",
          "province": "California",
          "provinceCode": "CA",
          "country": "United States",
          "countryCode": "US",
          "zip": "94102"
        }
      },
      "revokedAt": null,
      "revokedReason": null
    },
    "deliveryMethod": {
      "__typename": "SubscriptionDeliveryMethodShipping",
      "address": {
        "firstName": "John",
        "lastName": "Doe",
        "name": "John Doe",
        "address1": "123 Main Street",
        "address2": "Apt 4B",
        "city": "San Francisco",
        "company": "Acme Corp",
        "province": "California",
        "provinceCode": "CA",
        "country": "United States",
        "countryCode": "US",
        "countryCodeV2": "US",
        "zip": "94102",
        "phone": "+1-555-123-4567"
      },
      "shippingOption": {
        "title": "Standard Shipping",
        "presentmentTitle": "Standard Shipping (5-7 days)",
        "description": "Delivery within 5-7 business days",
        "code": "STANDARD"
      }
    },
    "originOrder": {
      "id": "gid://shopify/Order/77777",
      "name": "#1001",
      "fulfillmentOrders": {
        "nodes": [
          {
            "id": "gid://shopify/FulfillmentOrder/88888",
            "status": "FULFILLED",
            "fulfillAt": "2026-01-16T14:00:00Z"
          }
        ],
        "pageInfo": {
          "hasNextPage": false,
          "hasPreviousPage": false,
          "startCursor": "eyJsYXN0X2lkIjo4ODg4OH0",
          "endCursor": "eyJsYXN0X2lkIjo4ODg4OH0"
        }
      }
    },
    "discounts": {
      "nodes": [
        {
          "id": "gid://shopify/SubscriptionManualDiscount/44444",
          "title": "Loyalty Discount",
          "type": "MANUAL",
          "targetType": "LINE_ITEM",
          "recurringCycleLimit": null,
          "rejectionReason": null,
          "value": {
            "__typename": "SubscriptionDiscountPercentageValue",
            "percentage": 10.0
          }
        }
      ],
      "pageInfo": {
        "hasNextPage": false,
        "hasPreviousPage": false,
        "startCursor": "eyJsYXN0X2lkIjo0NDQ0NH0",
        "endCursor": "eyJsYXN0X2lkIjo0NDQ0NH0"
      }
    },
    "note": "Customer prefers delivery in the morning",
    "customAttributes": [
      {
        "key": "subscription_source",
        "value": "online_store"
      },
      {
        "key": "preferred_delivery_time",
        "value": "morning"
      }
    ],
    "billingAttempts": {
      "nodes": [
        {
          "id": "gid://shopify/SubscriptionBillingAttempt/99999",
          "createdAt": "2026-01-15T10:30:00Z",
          "completedAt": "2026-01-15T10:30:15Z",
          "idempotencyKey": "billing-attempt-12345-1",
          "ready": true
        }
      ]
    }
  }
}

Alternative Payment Methods:

PayPal Billing Agreement
"customerPaymentMethod": {
  "id": "gid://shopify/CustomerPaymentMethod/66666",
  "instrument": {
    "__typename": "CustomerPaypalBillingAgreement",
    "paypalAccountEmail": "customer@example.com",
    "inactive": false,
    "isRevocable": true,
    "billingAddress": {
      "name": "John Doe",
      "address1": "123 Main Street",
      "city": "San Francisco",
      "province": "California",
      "provinceCode": "CA",
      "country": "United States",
      "countryCode": "US",
      "zip": "94102"
    }
  },
  "revokedAt": null,
  "revokedReason": null
}
Shop Pay Agreement
"customerPaymentMethod": {
  "id": "gid://shopify/CustomerPaymentMethod/66666",
  "instrument": {
    "__typename": "CustomerShopPayAgreement",
    "name": "John Doe",
    "expiresSoon": false,
    "expiryMonth": 12,
    "expiryYear": 2028,
    "lastDigits": "4242",
    "maskedNumber": "•••• •••• •••• 4242",
    "isRevocable": true,
    "billingAddress": {
      "firstName": "John",
      "lastName": "Doe",
      "address1": "123 Main Street",
      "city": "San Francisco",
      "province": "California",
      "provinceCode": "CA",
      "country": "United States",
      "countryCode": "US",
      "zip": "94102"
    }
  },
  "revokedAt": null,
  "revokedReason": null
}

Alternative Delivery Methods:

Local Delivery
"deliveryMethod": {
  "__typename": "SubscriptionDeliveryMethodLocalDelivery",
  "address": {
    "firstName": "John",
    "lastName": "Doe",
    "name": "John Doe",
    "address1": "123 Main Street",
    "address2": "Apt 4B",
    "city": "San Francisco",
    "company": null,
    "province": "California",
    "provinceCode": "CA",
    "country": "United States",
    "countryCode": "US",
    "countryCodeV2": "US",
    "zip": "94102",
    "phone": "+1-555-123-4567"
  },
  "localDeliveryOption": {
    "title": "Same Day Delivery",
    "presentmentTitle": "Same Day Delivery",
    "description": "Delivered within 24 hours",
    "code": "SAME_DAY",
    "phone": "+1-555-999-0000"
  }
}
Pickup
"deliveryMethod": {
  "__typename": "SubscriptionDeliveryMethodPickup",
  "pickupOption": {
    "title": "Store Pickup",
    "presentmentTitle": "Pick up at Downtown Store",
    "description": "Available for pickup within 2 hours",
    "code": "PICKUP_DOWNTOWN",
    "location": {
      "id": "gid://shopify/Location/11111",
      "name": "Downtown Store",
      "address": {
        "address1": "456 Market Street",
        "address2": null,
        "city": "San Francisco",
        "country": "United States",
        "countryCode": "US",
        "province": "California",
        "provinceCode": "CA",
        "zip": "94103",
        "phone": "+1-555-888-0000"
      }
    }
  }
}

Billing Attempt Events

The following events send billing attempt data:

  • subscription.billing-success
  • subscription.billing-failure
  • subscription.billing-skipped
  • subscription.upcoming-order-notification
✅ subscription.billing-success Payload
{
  "type": "subscription.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,
    "graphOrderId": "gid://shopify/Order/77777",
    "orderId": 77777,
    "orderName": "#1002",
    "orderAmount": 59.30,
    "orderAmountUSD": 59.30,
    "orderAmountContractCurrency": 59.30,
    "orderNote": "Subscription order - Feb 2026",
    "retryingNeeded": false,
    "transactionFailedEmailSentStatus": null,
    "upcomingOrderEmailSentStatus": "SENT",
    "transactionFailedSmsSentStatus": null,
    "upcomingOrderSmsSentStatus": "SENT",
    "securityChallengeSentStatus": null,
    "billingAttemptResponseMessage": null,
    "applyUsageCharge": true,
    "recurringChargeId": 123,
    "transactionRate": 0.015,
    "usageChargeStatus": "APPLIED",
    "progressAttemptCount": 0,
    "lastShippingUpdatedAt": "2026-02-15T10:35:00Z",
    "inventorySkippedAttemptCount": 0,
    "inventorySkippedRetryingNeeded": false,
    "partialLinesSkipped": null
  }
}
❌ subscription.billing-failure Payload
{
  "type": "subscription.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,
    "graphOrderId": null,
    "orderId": null,
    "orderName": null,
    "orderAmount": null,
    "orderAmountUSD": null,
    "orderAmountContractCurrency": null,
    "orderNote": null,
    "retryingNeeded": true,
    "transactionFailedEmailSentStatus": "SENT",
    "upcomingOrderEmailSentStatus": null,
    "transactionFailedSmsSentStatus": "SENT",
    "upcomingOrderSmsSentStatus": null,
    "securityChallengeSentStatus": null,
    "billingAttemptResponseMessage": "INVALID_PAYMENT_METHOD: The payment method is invalid. Please update your payment information.",
    "applyUsageCharge": false,
    "recurringChargeId": 123,
    "transactionRate": 0.015,
    "usageChargeStatus": "NOT_APPLIED",
    "progressAttemptCount": 0,
    "lastShippingUpdatedAt": null,
    "inventorySkippedAttemptCount": 0,
    "inventorySkippedRetryingNeeded": false,
    "partialLinesSkipped": null
  }
}

Common Error Codes in billingAttemptResponseMessage:

  • INVALID_PAYMENT_METHOD: Payment method is invalid or expired
  • INSUFFICIENT_FUNDS: Insufficient funds in account
  • AUTHENTICATION_REQUIRED: Customer needs to authenticate payment
  • CARD_DECLINED: Card was declined by issuer
  • EXPIRED_PAYMENT_METHOD: Payment method has expired
  • INVENTORY_ALLOCATIONS_NOT_FOUND: Products out of stock
  • PAYMENT_METHOD_VERIFICATION_FAILED: Payment verification failed
⏭️ subscription.billing-skipped Payload
{
  "type": "subscription.billing-skipped",
  "data": {
    "id": 98767,
    "shop": "example-store.myshopify.com",
    "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99997",
    "contractId": 12345,
    "status": "SKIPPED",
    "billingDate": "2026-04-15T00:00:00Z",
    "attemptTime": "2026-04-15T10:30:00Z",
    "attemptCount": 1,
    "graphOrderId": null,
    "orderId": null,
    "orderName": null,
    "orderAmount": null,
    "orderAmountUSD": null,
    "orderAmountContractCurrency": null,
    "orderNote": null,
    "retryingNeeded": false,
    "transactionFailedEmailSentStatus": null,
    "upcomingOrderEmailSentStatus": null,
    "transactionFailedSmsSentStatus": null,
    "upcomingOrderSmsSentStatus": null,
    "securityChallengeSentStatus": null,
    "billingAttemptResponseMessage": "INVENTORY_ALLOCATIONS_NOT_FOUND: One or more products are out of stock.",
    "applyUsageCharge": false,
    "recurringChargeId": 123,
    "transactionRate": 0.015,
    "usageChargeStatus": "NOT_APPLIED",
    "progressAttemptCount": 0,
    "lastShippingUpdatedAt": null,
    "inventorySkippedAttemptCount": 1,
    "inventorySkippedRetryingNeeded": true,
    "partialLinesSkipped": "OUT_OF_STOCK"
  }
}
🔔 subscription.upcoming-order-notification Payload
{
  "type": "subscription.upcoming-order-notification",
  "data": {
    "id": 98768,
    "shop": "example-store.myshopify.com",
    "billingAttemptId": null,
    "contractId": 12345,
    "status": "PENDING",
    "billingDate": "2026-05-15T00:00:00Z",
    "attemptTime": null,
    "attemptCount": 0,
    "graphOrderId": null,
    "orderId": null,
    "orderName": null,
    "orderAmount": null,
    "orderAmountUSD": null,
    "orderAmountContractCurrency": null,
    "orderNote": null,
    "retryingNeeded": false,
    "transactionFailedEmailSentStatus": null,
    "upcomingOrderEmailSentStatus": "SENT",
    "transactionFailedSmsSentStatus": null,
    "upcomingOrderSmsSentStatus": "SENT",
    "securityChallengeSentStatus": null,
    "billingAttemptResponseMessage": null,
    "applyUsageCharge": false,
    "recurringChargeId": 123,
    "transactionRate": 0.015,
    "usageChargeStatus": null,
    "progressAttemptCount": 0,
    "lastShippingUpdatedAt": null,
    "inventorySkippedAttemptCount": 0,
    "inventorySkippedRetryingNeeded": false,
    "partialLinesSkipped": null
  }
}

Field Descriptions

Subscription Contract Fields

FieldTypeDescription
idStringShopify GraphQL ID of the subscription contract
statusEnumContract status: ACTIVE, PAUSED, CANCELLED, EXPIRED, FAILED
nextBillingDateDateISO 8601 date of next billing (YYYY-MM-DD)
createdAtDateTimeISO 8601 timestamp when subscription was created
updatedAtDateTimeISO 8601 timestamp of last update
lastPaymentStatusEnumLast payment status: SUCCEEDED, FAILED, null
billingPolicy.intervalEnumBilling frequency: DAY, WEEK, MONTH, YEAR
billingPolicy.intervalCountIntegerNumber of intervals between billings (e.g., 2 for every 2 months)
billingPolicy.maxCyclesIntegerMaximum billing cycles before auto-cancellation (null = unlimited)
billingPolicy.minCyclesIntegerMinimum billing cycles before customer can cancel
lines.nodesArraySubscription line items (products/variants)
customerObjectCustomer details (ID, name, email, phone)
customerPaymentMethodObjectPayment method (credit card, PayPal, Shop Pay) with billing address
deliveryMethodObjectDelivery method (shipping, local delivery, or pickup) with address
discounts.nodesArrayApplied discounts (percentage or fixed amount)
noteStringInternal subscription note
customAttributesArrayCustom key-value attributes

Billing Attempt Fields

FieldTypeDescription
idLongInternal database ID
shopStringShopify store domain
billingAttemptIdStringShopify GraphQL billing attempt ID
contractIdLongRelated subscription contract ID (without gid:// prefix)
statusEnumSUCCESS, FAILURE, SKIPPED, PENDING
billingDateDateTimeISO 8601 scheduled billing date
attemptTimeDateTimeISO 8601 actual attempt timestamp
attemptCountIntegerNumber of billing attempts for this cycle
graphOrderIdStringShopify GraphQL order ID (if successful)
orderIdLongShopify numeric order ID (if successful)
orderNameStringOrder name like "#1234" (if successful)
orderAmountDoubleOrder total in shop currency
orderAmountUSDDoubleOrder total in USD
orderAmountContractCurrencyDoubleOrder total in contract currency
retryingNeededBooleanWhether automatic retry is scheduled
billingAttemptResponseMessageStringError message if failed/skipped, null if successful
transactionFailedEmailSentStatusEnumEmail notification status for failures
upcomingOrderEmailSentStatusEnumEmail notification status for upcoming orders
inventorySkippedAttemptCountIntegerCount of skips due to inventory issues
partialLinesSkippedEnumReason for partial line skipping: OUT_OF_STOCK, PRICE_CHANGE, etc.

Signature Verification

Every webhook request is signed by Svix. You must verify the signature to ensure the request is authentic and hasn't been tampered with.

Svix includes these headers in every webhook request:

  • svix-id: Unique message ID
  • svix-timestamp: Unix timestamp of when the message was sent
  • svix-signature: Cryptographic signature

Verification Example (Node.js)

const { Webhook } = require('svix');

const secret = 'whsec_your_webhook_signing_secret';

app.post('/webhooks/appstle', (req, res) => {
  const payload = JSON.stringify(req.body);
  const headers = {
    'svix-id': req.headers['svix-id'],
    'svix-timestamp': req.headers['svix-timestamp'],
    'svix-signature': req.headers['svix-signature'],
  };

  const wh = new Webhook(secret);
  let event;

  try {
    event = wh.verify(payload, headers);
  } catch (err) {
    return res.status(400).send('Webhook signature verification failed');
  }

  // Process the verified webhook
  console.log('Event type:', event.type);
  console.log('Event data:', event.data);

  res.status(200).send('OK');
});

Verification Examples in Other Languages

Find your webhook signing secret in your Appstle dashboard under SettingsWebhooks.

Testing Webhooks

Using Svix Play

Svix provides a testing tool to send sample webhooks to your endpoint:

  1. Go to your Appstle dashboard → SettingsWebhooks
  2. Click on your endpoint
  3. Use the "Send Example" feature to send test events
  4. Verify your endpoint receives and processes the webhook correctly

Local Development

Use tools like ngrok or localtunnel to expose your local development server and test webhooks:

ngrok http 3000

Then add your ngrok URL (e.g., https://abc123.ngrok.io/webhooks/appstle) as a webhook endpoint in your Appstle dashboard.

Retry Schedule

If your endpoint doesn't return a 2xx status code, Svix automatically retries the webhook delivery:

  • Retry Schedule: Exponential backoff over 5 attempts across 3 days
  • Manual Retry: You can manually retry failed webhooks from the Svix dashboard
  • Endpoint Disabling: Endpoints are automatically disabled after sustained failures to prevent wasted resources

View delivery attempts and retry history in your Appstle dashboard under SettingsWebhooksMessage Logs.

Troubleshooting

Common Issues

IssueSolution
Signature verification failsEnsure you're using the correct webhook signing secret from your dashboard. Don't modify the raw request body before verification.
TimeoutsReturn 200 OK immediately and process events asynchronously. Avoid long-running operations in your webhook handler.
Wrong status codesAlways return 2xx for successful receipt (even if you encounter business logic errors). Use 4xx only for malformed requests.
CSRF protection blocking webhooksExempt your webhook endpoint from CSRF checks in your web framework.
Duplicate eventsWebhooks can be delivered more than once. Use the svix-id header to make your processing idempotent.

Debugging Tips

  • Check the Message Logs in your Appstle dashboard to see delivery attempts and responses
  • Log the raw webhook payload to understand the exact data structure
  • Verify your endpoint is publicly accessible (not behind VPN/firewall)
  • Test with a simple endpoint that just logs and returns 200 OK

Need Help?

Contact support@appstle.com with your endpoint URL and example svix-id for assistance.


Last updated: January 2026