# Appstle Webhooks

Appstle Subscriptions (a Shopify subscriptions app) sends webhooks signed with
Svix's scheme. Every delivery is an HTTP `POST` with a JSON body
shaped `{ "type": "subscription.created", "data": { ... } }` and three signature headers:
`svix-id`, `svix-timestamp`, and `svix-signature`. Webhooks are included in Appstle's
Enterprise and Enterprise Plus plans.

## When to Use This Skill

* How do I receive Appstle webhooks?
* How do I verify Appstle webhook signatures (`svix-signature`)?
* How do I handle `subscription.created` or `subscription.cancelled` events?
* How do I react to failed payments (`subscription.billing-failure`)?
* Why is my Appstle webhook signature verification failing, or failing only on retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with
HMAC-SHA256 using the base64-decoded secret (the part after `whsec_`), base64-encodes
the result, and sends one or more space-separated `v1,<signature>` entries. Appstle's
docs show a hand-written verifier that also rejects a `svix-timestamp` more than five
minutes from your clock. The official `svix` library implements the same scheme and check,
and compares in constant time.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const wh = new Webhook(process.env.APPSTLE_WEBHOOK_SECRET); // "whsec_..."
// rawBody: the raw Buffer/string, never re-serialized JSON
wh.verify(rawBody, {
  'svix-id': req.headers['svix-id'],
  'svix-timestamp': req.headers['svix-timestamp'],
  'svix-signature': req.headers['svix-signature'],
}); // throws WebhookVerificationError on a bad signature or stale timestamp
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["APPSTLE_WEBHOOK_SECRET"])
wh.verify(raw_body, {"svix-id": msg_id, "svix-timestamp": ts, "svix-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/appstle-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/appstle-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/appstle-webhooks/examples/fastapi/).

### Timestamp window and Hookdeck retries

The `svix` (and `standardwebhooks`) libraries always enforce a fixed **5-minute** timestamp
window and offer no option to change it. Appstle's own retries carry a new timestamp, but
Hookdeck retries and replays resend the **original** `svix-timestamp`, so a retry or replay
that arrives more than 5 minutes after the event fails library verification even though the
signature is valid. If you receive
Appstle webhooks through Hookdeck, or otherwise need to accept late retries, use the
manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/appstle-webhooks/references/verification.md): HMAC-SHA256
over `{id}.{timestamp}.{raw body}`, key = base64-decode of the secret after `whsec_`,
compare every space-separated `v1,<base64>` entry in constant time. It lets you drop the
window or widen it.

## Common Event Types

The `type` field of the body carries the event name. Appstle documents these:

| Event | Triggered When | `data` |
| --- | --- | --- |
| `subscription.created` | A new subscription is created | Subscription contract |
| `subscription.updated` | Subscription details change | Subscription contract |
| `subscription.activated` | A subscription is activated | Subscription contract |
| `subscription.paused` | A subscription is paused | Subscription contract |
| `subscription.cancelled` | A subscription is cancelled | Subscription contract |
| `subscription.next-order-date-changed` | The next order date is changed | Subscription contract |
| `subscription.billing-interval-changed` | The billing frequency changes | Subscription contract |
| `subscription.billing-success` | A payment is processed | Billing attempt |
| `subscription.billing-failure` | A payment try fails (sent after every failed try) | Billing attempt |
| `subscription.billing-skipped` | A billing cycle is skipped | Billing attempt |
| `subscription.upcoming-order-notification` | A queued order comes inside its notice period | Billing attempt with contract fields |

Always branch on `type` and ignore types you don't handle: the list can grow. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/appstle-webhooks/references/overview.md) for payload fields.

## Environment Variables

```bash
# More → Webhook Integration → Launch webhook portal → your endpoint (starts with whsec_)
APPSTLE_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 appstle --path /webhooks/appstle

```

Register the tunnel URL in the webhook portal (**More → Webhook Integration → Launch webhook
portal**), then open the endpoint and send it an example event.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/appstle-webhooks/references/overview.md) - Appstle webhook concepts, event list, payloads, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/appstle-webhooks/references/setup.md) - Register an endpoint and get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/appstle-webhooks/references/verification.md) - SDK and manual verification, timestamp window, gotchas