hookwright
v2.3.7
Published
Build real, signed e-commerce webhooks from a live Shopify catalogue — interactive terminal UI, no backend
Maintainers
Readme
hookwright
Build real, signed e-commerce webhooks from a live Shopify catalogue — and send them to your own endpoint.
Nothing in the payload is invented. Product titles, variant ids, prices and image URLs all come from your Shopify store through the Admin API; totals are computed from those real prices. It is a terminal app built with Ink — there is no backend and no service to sign up for.
╭──────────────────────────────────────────────────────────────╮
│ hookwright Addy Fashion │
╰──────────────────────────────────────────────────────────────╯
real catalogue → signed provider webhooks
❯ Setup Shopify and webhook destination
Integrations build and send a provider webhook
History review or re-send a saved payload
Doctor check credentials and connectivity
Clear remove cached data
QuitInstall
npx hookwright # no install
npm i -g hookwright # or install itRequires Node 20+.
Quick start
hookwright # interactive UI — starts in the setup wizard on first runSetup and Integrations sit side by side. Setup is global — configure it once and every integration uses it:
| Setup | asks for |
|---|---|
| Shopify | store domain + Admin API access token (needs read_products). Verifies immediately and picks up currency, country, province and contact details from the store record |
| Webhook | one destination URL + signing secret per integration — Cashfree, Razorpay Magic and Nitro each get their own |
| Customer | the shopper the abandoned cart belongs to |
| Defaults | currency, platform |
Stored in ~/.config/hookwright/config.json, mode 0600 — it holds a Shopify token and a signing secret.
Then Integrations → Cashfree One Click Checkout builds the payload:
- Products are fetched live from your store and listed — pick what goes in the cart
- Variant and quantity per product
- Cart discount
- Either accept the suggested values, or review every field — each one is pre-filled with the generated suggestion, enter accepts it, typing replaces it
- Pre-flight report — every check, the full field coverage table, the signed request and the complete payload
- A ready-to-paste
curlis copied to your clipboard automatically, then send
The report page is a compact status board with a command bar — s send · c copy curl · j copy json · u curl · p payload · f fields · v checks · r request. Panels are collapsed by default, so you see the verdict first and the detail only when you ask for it.
esc walks back one step at a time through the whole flow (report → mode → discount → quantity → variant → products → event), and only leaves the integration from the first step.
The curl is built for pasting
The command is emitted in the exact shape Postman itself exports — curl --location --request POST with unindented --header / --data lines — and the body is inlined, never @file. Drop it straight into Postman → Import → Raw text, a colleague's terminal, or a bug report.
Single quotes inside product titles (Levi's 501) are escaped, and the signature travels with the exact body it signs, so a pasted command verifies precisely as the original request would.
Two things keep this honest: a test that executes the generated command verbatim against a receiver doing real HMAC verification, and a test that runs it through curl-to-postmanv2 — the same parser Postman uses for Raw-text import — asserting the URL, method, every header and a byte-identical body all survive.
In the report screen: c re-copies the curl · j copies the raw payload JSON · p toggles the full payload view.
Headless usage
Every command works without a TTY, for CI or scripting:
hookwright build --items 2 # cashfree-occ by default
hookwright build --provider razorpay-magic --items 2
hookwright build --provider nitro --event orders/create
hookwright build --provider nitro --event category_view --collection summer-sale
hookwright build --provider nitro --event view # no product needed
hookwright build --search "cold brew" # only products matching a title
hookwright build --items 3 --discount 250 --send
hookwright send --dry-run # print the request + curl, send nothing
hookwright send --file ~/.local/state/hookwright/payloads/2026-…json
hookwright doctor --json
hookwright history
hookwright clear --payloads --yes # or --logs / --shop-cache / --disconnect / --allExit codes: 0 ok · 1 configuration/usage · 2 pre-flight failed · 3 endpoint rejected the webhook.
What it checks before sending
A generated payload is useless if the receiver silently drops it, so every build runs pre-flight:
- Schema gate —
typemust equalABANDONED_CHECKOUT, or the consumer ignores the event without an error. - JSON round-trip — receivers that verify
HMAC(secret, timestamp + JSON.stringify(parsedBody))can only match ifJSON.stringify(JSON.parse(raw)) === raw. If it doesn't, you're told the exact byte offset. - Phone — parsed to E.164 using the shipping
country_code, the same way the consumer does. - Phone allow-list — refuses to build for a number that isn't an approved test handset, so a live shopper can never be messaged by a test run.
- Product images — every
image_urlisHEAD-checked. A product with no image warns rather than blocking: the message simply renders without one. - Endpoint exists — before a signed payload is handed over, the destination is verified: the hostname must resolve, something must accept a TCP connection on the port, and the path must not 404. A typo'd host is caught here instead of looking like a silent delivery failure. The probe is
OPTIONS, falling back toHEAD— never a POST and never a body, so it cannot create data on the receiving system. Timeouts and servers that rejectOPTIONSwarn rather than block. Skip with--no-endpoint-check, override with--force. - Field coverage — a table of every field the consumer reads, what it maps to, and its value. Missing required fields are flagged in red.
Sending is also blocked when the target looks like production, unless --force.
Safety
- The config file is
0600and secrets are redacted from the log file. - Dry-run prints a
curlthat reproduces the request exactly, using--data-binaryso the signed bytes are preserved. - Payload history lives in
~/.local/state/hookwright/payloads/and can be re-sent (re-signed with a fresh timestamp).
Override both locations with HOOKWRIGHT_HOME, or point at one config with --config <path> / HOOKWRIGHT_CONFIG.
Clear removes local state — saved payloads, the activity log, the cached store record, the Shopify credentials, or everything back to first-run state. It shows exactly what will be deleted and asks before doing it.
Providers
| provider | signature | headers | events |
|---|---|---|---|
| cashfree-occ | base64 HMAC-SHA256 over timestamp + body | x-webhook-signature, x-webhook-timestamp | abandoned checkout |
| razorpay-magic | hex HMAC-SHA256 over the body | x-razorpay-signature | abandoned checkout |
| nitro | static bearer token, no body signature | authorization | 8 — page view, category view, product view, add to cart, remove from cart, checkout, order created, order updated |
Each provider owns its gate, signature scheme, field map, payload shape and summary, so nothing is assumed across them:
- Cashfree OCC nests everything under
data; currency is read fromline_items[0], not the root. - Razorpay Magic is root-level, uses capitalised shipping keys (
Shipping_address.Address1) and sendsline_items_totalas a string in paise — matching Razorpay's abandoned cart webhook docs. Its gate is "non-emptyabandoned_checkout_url" rather than an exact value. - Nitro keys off
eventNameand requireseventVal.customer.phoneon every event, parsed against rootcountry. Choose the event with--event, or from the menu in the UI.
Nitro's eight events are about different things, so each asks for a different subject:
| event | you pick | cart |
|---|---|---|
| view | nothing | — |
| category_view | one collection (fetched from Shopify, custom and smart) | — |
| product_view | one product | — |
| addtocart · removefromcart · checkout · orders/create · orders/updated | products and quantities | ✅ |
A page view has no cart, so it is never asked for a discount; a category view emits the collection's real id, title and URL and carries no line items.
Adding one is a single file in src/providers/ exporting buildPayload, sign, webhookUrl, summarize, a gate and a fieldMap — see docs/cashfree-occ.md.
Development
npm install
npm run dev # esbuild watch
npm test # 190 tests, no network required
npm start # build + runThe test suite includes an end-to-end run against a stub Shopify API and a stub consumer that verifies the signature exactly as the real one does, asserting the transmitted bytes are identical to the signed bytes.
License
MIT
