# Task: connect this store to Watx
Watx is the WhatsApp CRM this business uses. Connect this codebase's store to it so that:
1. Watx receives the store's orders, carts and products as they change. Watx then messages customers on WhatsApp.
2. Watx can ask the store to create a discount code, and to cancel or tag a cash-on-delivery order.
Use this codebase's own language, framework and patterns for configuration, background jobs, routing and tests. Ask me before adding a new dependency.
## Reference
- Guide: https://docs.watx.in/integrations/custom-store
- JSON Schema for every payload: https://api.watx.in/webhooks/store/schema.json
If you can fetch URLs, read both first. Everything you need is also below.
## Configuration
Read these from environment variables. Never hard-code them, never log the secret, never send it to a browser:
- WATX_WEBHOOK_URL = <the webhook URL shown in Watx>
- WATX_SECRET = the signing secret (starts with whsec_). I will set it myself.
## Part 1: send events to Watx
Write one server-side function, e.g. sendToWatx(event, data):
1. Serialise the body once: {"event": <event>, "data": <data>}.
2. Sign those exact bytes: HMAC-SHA256 with WATX_SECRET, hex digest. Send header X-Watx-Signature: sha256=<hex> and Content-Type: application/json. Never re-serialise after signing.
3. POST it to WATX_WEBHOOK_URL with a 10-second timeout.
4. Watx always answers HTTP 200 with {"received": true, "outcome": "..."}. Log the outcome. These outcomes are bugs to fix: "bad signature", "unknown event ...", "not a Watx envelope ...", and anything containing " — check: " (Watx lists the fields it could not use).
5. Never slow down or fail a checkout: send after the database transaction commits, from a background job or queue if the codebase has one. Retry network errors and 5xx responses up to 3 times with backoff. Sending the same event twice is safe; Watx de-duplicates.
Call it where these things happen:
| When | event | data |
| --- | --- | --- |
| An order is placed | order.created | Order |
| Payment is captured | order.paid | Order |
| The order ships | order.fulfilled | Order |
| The order is cancelled | order.cancelled | Order |
| The order is refunded | order.refunded | Order |
| Any other change to an order | order.updated | Order |
| A cart is created or changes, once a phone or email is known | checkout.updated | Checkout |
| A cart is emptied or deleted | checkout.deleted | {"id"} |
| A product is created or changes | product.updated | Product |
| A product is deleted | product.deleted | {"id"} |
Always send the whole current object, not only what changed.
### Data shapes
Field names are exact. Amounts are MAJOR units as decimal strings ("1499.00"). Ids are strings. Phones are international format ("+919876543210"). Times are ISO 8601 with an offset. Leave out a field you do not have rather than sending an empty string. * = required.
Order:
- id* string: your order id, the same on every event
- number string: what the customer sees, e.g. "#1042"
- created_at datetime; cancelled_at datetime (order.cancelled only)
- currency string: ISO 4217, e.g. "INR"
- total* money: the amount charged
- subtotal, discount, shipping, tax: money
- taxes_included boolean
- payment_method string: "cod" for cash on delivery, otherwise the gateway name
- payment_status string: pending | paid | partially_paid | refunded | partially_refunded | voided
- fulfillment_status string: unfulfilled | partial | fulfilled
- status_url url: the customer's order page
- customer object: name, first_name, last_name, phone, email. Without a phone nobody can be messaged.
- shipping_address object: line1*, line2, city, state, postal_code, country_code (2 letters, e.g. "IN")
- items array of {title*, quantity (integer), price (money, the UNIT price)}
- shipping_method string
- checkout_id string: the id of the cart this order came from
- discount_codes array of strings: codes used on the order
Checkout: id* string; currency; total* money; items (as above); recovery_url url: the link that reopens THIS cart, which the reminder is built around; customer {name, phone, email, accepts_marketing boolean}; created_at, updated_at datetime.
Product: id* string; title* string; description string; price* money; image_url url; url url (the product page); inventory integer or null (null = not tracked); active boolean (false removes it from Watx).
Also write:
- a command that sends "ping" with data {} and prints the outcome, which should be "pong";
- a one-off script that sends product.updated for every existing product, to fill the catalogue.
## Part 2: receive requests from Watx
Add a public HTTPS route, e.g. POST /watx/callback:
1. Read the RAW request body before any JSON parsing.
2. Verify X-Watx-Signature: HMAC-SHA256 of the raw body with WATX_SECRET, hex, optional "sha256=" prefix, constant-time comparison. Answer 401 when it does not match.
3. Parse {"id", "event", "created_at", "data"}. Ignore an id you have already handled.
4. Handle:
- ping: nothing to do.
- coupon.create: create a single-use discount code that is EXACTLY data.code (match it case-insensitively at checkout), data.percent_off percent off everything, valid from data.starts_at until data.expires_at, usable once (data.usage_limit), and only on orders of at least data.minimum_order when that is not null. Watx has already sent this code to the customer, so it must work. If the code already exists, treat it as done.
- order.cancel: cancel order data.order_id. It is cash on delivery: restock, nothing to refund.
- order.tag: add data.tags to order data.order_id (a note is fine if orders have no tags).
5. Answer 2xx when done, within 10 seconds. On failure answer 4xx or 5xx with JSON {"error": "<a sentence a shop owner can read>"}; Watx shows that sentence to the merchant.
## Tests
- The signature of a fixed body and secret equals the known HMAC-SHA256 hex digest.
- The order, cart and product mappers produce the shapes above (validate against the JSON Schema if the codebase has a validator).
- The callback route answers 401 to a wrong signature, and creates the code for a correctly signed coupon.create.
## When you are done, tell me
1. Where sendToWatx is called from, event by event.
2. The callback route's path, so I can enter its full https URL in Watx (Integrations → Custom store → Connection settings → Callback URL).
3. Which environment variables to set.
Done means: the ping prints "pong" and the store shows Connected in Watx; a test order returns an outcome starting "order placed" with no " — check: "; and Watx's "Send a test" on the Callback URL step succeeds.