{"_id":"@cartgenie/webhooks","name":"@cartgenie/webhooks","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@cartgenie/webhooks","version":"0.1.0","description":"TypeScript SDK for receiving, verifying, and dispatching CartGenie store webhook events.","keywords":["cartgenie","webhooks","ecommerce","sdk"],"license":"MIT","author":{"name":"Marketing Fire LLC"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./express":{"import":{"types":"./dist/express.d.ts","default":"./dist/express.js"},"require":{"types":"./dist/express.d.cts","default":"./dist/express.cjs"}},"./next":{"import":{"types":"./dist/next.d.ts","default":"./dist/next.js"},"require":{"types":"./dist/next.d.cts","default":"./dist/next.cjs"}},"./web":{"import":{"types":"./dist/web.d.ts","default":"./dist/web.js"},"require":{"types":"./dist/web.d.cts","default":"./dist/web.cjs"}},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/monto/cartgenie-webhooks-sdk.git"},"sideEffects":false,"engines":{"node":">=20"},"scripts":{"build":"tsup","prepublishOnly":"npm run build","test":"vitest run","test:watch":"vitest","lint":"eslint .","typecheck":"tsc --noEmit","format":"prettier --write .","format:check":"prettier --check .","example:express":"tsx examples/express.ts"},"devDependencies":{"@eslint/js":"^9.14.0","@types/express":"^5.0.0","@types/node":"^22.9.0","eslint":"^9.14.0","express":"^4.21.1","prettier":"^3.3.3","tsup":"^8.3.5","tsx":"^4.19.2","typescript":"^5.9.0","typescript-eslint":"^8.14.0","vitest":"^2.1.4"},"_id":"@cartgenie/webhooks@0.1.0","gitHead":"b39ce2217991e5b70ddc11a5f642d00bd7417f36","bugs":{"url":"https://github.com/monto/cartgenie-webhooks-sdk/issues"},"homepage":"https://github.com/monto/cartgenie-webhooks-sdk#readme","_nodeVersion":"22.11.0","_npmVersion":"10.9.0","dist":{"integrity":"sha512-kLSss8ZiIZRbFxBSHMEJHxhdLyEjd1zycbdwsF5HpikwN084P+YOThkQ9WUHJznBERemgP5Yr5Zngl6nB8inwA==","shasum":"89b7030e40dc79cbb04a4942a706756640e87814","tarball":"https://registry.npmjs.org/@cartgenie/webhooks/-/webhooks-0.1.0.tgz","fileCount":33,"unpackedSize":162220,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICtTC6eyRtXcV3jWkMB6WOq4UANEl8FfvVdEGRDwshg5AiEA49sg2yWLvP1VCGuzJRnofQ1wKih/iHkv+Ddd7YU7gDs="}]},"_npmUser":{"name":"zhildzik","email":"viktar@viralmediapartners.com"},"directories":{},"maintainers":[{"name":"creativeatx","email":"ryanmauldin@gmail.com"},{"name":"zhildzik","email":"viktar@viralmediapartners.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/webhooks_0.1.0_1788167387678_0.35644126235412754"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-31T09:09:47.471Z","0.1.0":"2026-08-31T09:09:47.825Z","modified":"2026-08-31T09:09:48.068Z"},"maintainers":[{"name":"creativeatx","email":"ryanmauldin@gmail.com"},{"name":"zhildzik","email":"viktar@viralmediapartners.com"}],"description":"TypeScript SDK for receiving, verifying, and dispatching CartGenie store webhook events.","homepage":"https://github.com/monto/cartgenie-webhooks-sdk#readme","keywords":["cartgenie","webhooks","ecommerce","sdk"],"repository":{"type":"git","url":"git+https://github.com/monto/cartgenie-webhooks-sdk.git"},"author":{"name":"Marketing Fire LLC"},"bugs":{"url":"https://github.com/monto/cartgenie-webhooks-sdk/issues"},"license":"MIT","readme":"# @cartgenie/webhooks\n\nTypeScript SDK for receiving CartGenie store webhooks: it verifies the\ndelivery signature against the raw request bytes, parses the envelope, and\ndispatches typed events to your handlers.\n\nWorks in Node.js ≥ 20 and in every runtime with Web Crypto and `fetch`\nprimitives — Deno, Bun, Cloudflare Workers, Vercel Edge. Zero runtime\ndependencies.\n\n## Install the CartGenie SDK\n\n```bash\nnpm install @cartgenie/webhooks\n```\n\n## Quickstart\n\n```ts\nimport { CartGenieWebhooks } from '@cartgenie/webhooks';\n\nconst cg = new CartGenieWebhooks({\n  // CartGenie dashboard → Settings → Webhooks → Signing secret\n  secret: process.env.CARTGENIE_WEBHOOK_SECRET!,\n});\n\ncg.on('new_order', async ({ payload }) => {\n  await recordOrder(payload.order_id, payload.total, payload.currency_code);\n});\n\ncg.on('new_abandoned_cart', async ({ payload }) => {\n  await sendRecoveryEmail(payload.email, payload.guid);\n});\n```\n\n### Next.js (App Router)\n\n```ts\n// app/api/cartgenie-webhook/route.ts\nimport { createNextRouteHandler } from '@cartgenie/webhooks/next';\n\nexport const POST = createNextRouteHandler(cg);\n```\n\n### Express\n\n```ts\nimport express from 'express';\nimport { createExpressWebhookMiddleware } from '@cartgenie/webhooks/express';\n\nconst app = express();\n\napp.post(\n  '/cartgenie-webhook',\n  express.raw({ type: 'application/json' }), // required: the signature covers the raw bytes\n  createExpressWebhookMiddleware(cg),\n);\n```\n\n### Any Web-standard runtime (Remix, Bun, Deno, Cloudflare Workers)\n\n```ts\nimport { createWebhookHandler } from '@cartgenie/webhooks/web';\n\nconst handler = createWebhookHandler(cg);\n\nexport default { fetch: handler };\n```\n\n### Without an adapter\n\n```ts\nimport { parseWebhook, isKnownWebhookEvent } from '@cartgenie/webhooks';\n\nconst event = await parseWebhook({\n  rawBody: await request.arrayBuffer(), // never re-serialized JSON\n  signature: request.headers.get('Signature'),\n  secret: process.env.CARTGENIE_WEBHOOK_SECRET!,\n});\n\nif (isKnownWebhookEvent(event) && event.type === 'inventory_updated') {\n  console.log(event.payload.stock);\n}\n```\n\n## Events\n\n| `type` | Payload | Fired when |\n| --- | --- | --- |\n| `new_order` | `OrderPayload` | An order is placed (not paid — offline payments arrive with `payment.status: \"unpaid\"`) |\n| `order_updated` | `OrderPayload` | Fulfillment status changes, the order is canceled, or tracking info is updated |\n| `order_fulfillment_updated` | `OrderPayload` | A fulfillment is created or changed |\n| `refund_issued` | `RefundIssuedPayload` | A refund is issued (`OrderPayload` plus `refund_amount`) |\n| `new_customer` / `customer_updated` | `CustomerPayload` | A customer is created / updated |\n| `new_subscription` / `subscription_updated` | `SubscriptionPayload` | A subscription is created / canceled, paused, resumed |\n| `new_subscription_charge` | `SubscriptionPayload` | A recurring subscription charge succeeds |\n| `new_discount` / `discount_updated` | `DiscountPayload` | A discount is created / updated or disabled |\n| `new_category` / `category_updated` | `CategoryPayload` | A category is created / updated |\n| `new_product` / `product_updated` | `ProductPayload` | A product is created / updated |\n| `new_abandoned_cart` | `AbandonedCartPayload` | A cart is considered abandoned |\n| `inventory_updated` | `InventoryPayload` | A variant's stock changes |\n\nCartGenie adds event types over time. Unknown types are parsed and routed to\n`onUnknown` handlers instead of failing, so older SDK versions keep working —\nuse `isKnownWebhookEvent()` when working with `parseWebhook()` directly.\n\n## Delivery contract\n\nEvery delivery is an HTTPS `POST` with body\n`{ \"type\": \"<event>\", \"payload\": { … } }` and two notable headers:\n\n- `Content-Type: application/json`\n- `Signature`: lowercase hex `HMAC-SHA256(rawBody, secret)`\n\nThe signature covers the **raw request bytes**. Never parse and re-serialize\nthe JSON before verifying — non-ASCII characters are `\\uXXXX`-escaped on the\nwire, and re-serialization changes the bytes. All adapters in this package\nread the raw body correctly.\n\nThe signing secret is per store and shared by all of the store's webhook\nsubscriptions (dashboard → Settings → Webhooks). There is no timestamp or\nnonce in the scheme, so the signature proves authenticity and integrity but\nnot freshness — treat replay protection as your application's concern if you\nneed it.\n\nWebhook destinations must be publicly reachable HTTPS URLs with valid\ncertificates; CartGenie refuses to deliver to private networks or localhost.\n\n## Reliability semantics\n\n- **Retries are the sender's job.** CartGenie attempts each delivery up to 3\n  times with exponential backoff and a 10-second response timeout. Your\n  receiver never needs to ask for a retry — return HTTP 500 (the adapters do\n  this when your handler throws) and the delivery is retried.\n- **Respond fast.** A response slower than 10 seconds counts as a failed\n  attempt. Acknowledge with 2xx quickly and do heavy work asynchronously.\n- **At-least-once, no ordering.** A slow-but-successful response or a queue\n  retry produces duplicate deliveries, and deliveries can arrive out of order.\n  Deliveries carry no event id, so deduplicate by natural keys in the payload\n  (order `order_id`/`guid`, variant `sku`, …) and key decisions off state\n  fields (e.g. `fulfillment_state`), never off arrival order.\n- **Unsubscribing via 410.** If your endpoint responds `410 Gone` to a\n  delivery attempt, CartGenie disables that webhook subscription immediately.\n- **Test deliveries.** \"Send test\" from the dashboard or API delivers sample\n  data through the real pipeline. Test payloads approximate real ones —\n  `inventory_updated.previous_stock` in particular is absent on tests.\n\n## Payload conventions worth knowing\n\n- Integer money fields are **hundredths of the major currency unit**\n  (`114000` → `$1,140.00`); locale-formatted `formatted_*` strings ride along.\n  The abandoned-cart payload carries formatted strings only.\n- Some fields are machine enum values (`order.status`, `payment.status`,\n  `activation_type`), others are human labels (`discount_type: \"Percentage\"`,\n  `product.status: \"Published\"`). The TypeScript types encode which is which.\n- Order payloads carry both `items` and `products` — historical duplicates\n  kept for backward compatibility.\n- `options` is a list of option rows on order/cart items but a flat string map\n  on `inventory_updated`.\n- PHP serializes empty maps and empty relations as `[]`: an option-less\n  variant's `options`, an order without a shipment (`shipment: []`), and empty\n  personalization/custom-field maps all arrive as empty arrays. `refunds` is\n  the one inverse case — it is `{}` unless the order is (partially) refunded,\n  then an array. The TypeScript types encode each duality.\n- Bundle component quantities on order items arrive pre-multiplied by the line\n  quantity.\n- `availability` and `estimated_shipping_date` are always present; their\n  values stay `\"available\"`/`null` unless the store uses pre-orders. Truly\n  conditional fields (`renewal_price`, `formatted_renewal_price`,\n  `previous_stock`) are optional in the types.\n\n## API\n\n- `new CartGenieWebhooks({ secret })` — client; `.on(type, handler)` for one\n  typed event, `.onAny(handler)` for every event (known and unknown — narrow\n  with `isKnownWebhookEvent`), `.onUnknown(handler)` for unrecognized types\n  only, `.handle({ rawBody, signature })`. Handlers run sequentially and are\n  awaited; handler errors reject `handle()`.\n- `verifySignature({ rawBody, signature, secret })` → `Promise<boolean>` —\n  constant-time HMAC check via Web Crypto.\n- `parseWebhook({ rawBody, signature, secret })` → `Promise<WebhookEvent>` —\n  throws `SignatureVerificationError` / `InvalidPayloadError`.\n- `isKnownWebhookEvent(event)` — narrows `WebhookEvent` to\n  `KnownWebhookEvent`.\n- Adapters: `@cartgenie/webhooks/express`, `@cartgenie/webhooks/next`,\n  `@cartgenie/webhooks/web`. All accept `onProcessed` / `onError` hooks; the\n  web/Next adapter also accepts `maxBodyBytes` (default 5 MiB → HTTP 413),\n  while the Express route is bounded by `express.raw()`'s own limit.\n\nAll payload interfaces (`OrderPayload`, `CustomerPayload`, …) are exported\nfrom the package root.\n\n## Not in scope (v1)\n\n- **No receiver-side retries or queues** — the sender retries; bring your own\n  queue for heavy processing.\n- **No replay protection** — the signing scheme has no timestamp.\n- **No idempotency store** — deliveries carry no event id; deduplicate by\n  natural keys.\n- **No REST API client** — webhook subscription management (create, update,\n  test-send) lives in the CartGenie public API.\n\n## Development\n\n```bash\nnpm install\nnpm test          # vitest, includes byte-exact fixtures generated by the platform\nnpm run lint\nnpm run typecheck\nnpm run build     # tsup → dist (ESM + CJS + d.ts)\n```\n\n`tests/fixtures/` contains one canonical delivery per event type — raw JSON\nbytes and `Signature` values produced by the CartGenie platform itself — so\nsignature verification and payload parsing are pinned against the real wire\nformat, not this package's assumptions.\n\n## Releasing\n\n1. Bump `version` in `package.json`.\n2. `npm run lint && npm run typecheck && npm test`.\n3. `npm publish` — `prepublishOnly` rebuilds `dist`, and `publishConfig.access`\n   is already `public`.\n4. Tag the version and create a GitHub Release.\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md","_rev":"1-a4fe348e46404f1e8e3c30412765e4fe"}