Home / Knowledge Library

How to connect the server webhook

Report sales your tag cannot see (renewals, late captures, phone orders) with a signed call from your own server, and read every reply it gives back.

By Nora Ellis, Affiliate Marketing & Creator Economy Writer · with Wooly Network, Advertiser Success · 3 September 2026 · 11 min · Tracking & Attribution

Your tracking tag already reports sales. This section is for the sales it cannot see; the ones confirmed somewhere no browser is present. A subscription renewing at 3am, a payment captured hours after checkout, an order taken over the phone, a signup your own backend completes.

It is the one thing on the Integration setup page that needs a developer, and the one thing that can pay a commission twice if it is wired up wrong. So this guide is written to be handed to whoever writes your backend code, and it says plainly what goes wrong and how to avoid it.

First: do you actually need this?

A browser was there means your tag already reports it; no browser anywhere means only your server knows

If a customer's browser is on a confirmation page when the sale completes, your tag already reports it and you should not send it again. Reporting the same sale twice gains you nothing and risks paying twice.

Send it from your server only when there is genuinely no browser:

The one mistake that costs real money. If your tag and your server both report the same sale, send the same orderId from both. We recognise a repeat and count it once. Two genuinely different strings are two orders, and both earn a commission. We spot that pattern and tell you when it happens, but we cannot merge them afterwards; merging references that merely look related would join genuinely different orders.

Use your customer-facing order number, the same one your tag sends: #1001, not Shopify's gid://shopify/Order/…, and not your payment gateway's transaction id. Spelling differences we do reconcile: #1001 and 1001 are one order to us.


Step 1: Capture the click reference at checkout

The wly reference arrives on the landing URL, is saved with the order, and is sent back in the wly field

This is the one job nothing here can do for you, and it comes before you touch the address.

A browser knows which creator sent the visitor. Your server does not. So the browser has to hand it over, and your order has to carry it.

  1. Someone arrives from a creator link. The reference is on the URL: ?wly=abc123.
  2. Read it and store it against the order. Either take it off the query string yourself, or ask the tag for it, the tag exposes wooly("ref"), which returns the reference it is holding for this visitor, including one saved from an earlier visit.
  3. When your server reports the sale, send that value back in the wly field.
// On your site, at checkout const ref = window.wooly ? wooly("ref") : new URLSearchParams(location.search).get("wly"); // save `ref` on the order in your database // Later, from your server { "event": "conversion", "orderId": "#1001", "amount": 149.00, "currency": "USD", "wly": "abc123" }

No wly does not break anything: the sale is still recorded and your totals are still right. It simply credits nobody. Most orders on most shops genuinely have no creator behind them, so that is often correct, but if it is every order, this step is not wired in.


Step 2: Turn the switch on and copy your address

The switch off and on, and the server reporting address with its secret masked

The address is refused until the switch is on. Turning it off again closes it immediately; nothing already recorded changes, and your tag is unaffected either way. That switch is your whole rollback.

With it on, copy the server reporting URL:

https://your-domain.com/api/track/conversion?key=wly_live_ab12cd34&secret=••••••••••••

Same rule as everywhere else on this page: key= is public and merely names your account, secret= is a password. It goes in your backend's settings or your secret manager and nowhere else, never on your website, never in a repository, never in an email.

The address changes if you rotate your tracking key. If you rotate, come back and paste the new one into your backend.


Step 3: Sign the exact bytes you send

Build the body once, take the time, sign time and body together, send both, and the right versus wrong way to do it

This is the half that goes wrong, and it goes wrong the same way every time. Four things happen in order:

  1. Build the body string once. Serialise your JSON into a string and hold onto that exact string.
  2. Take the current time in epoch seconds.
  3. Sign time + "." + body with HMAC-SHA256, using your secret as the key, and hex-encode it.
  4. Send both: the same body string as the request body, and the header X-Wooly-Signature: t=<time>,v1=<signature>.
SECRET="$WOOLY_SECRET" T=$(date +%s) BODY='{"event":"conversion","orderId":"#1001","amount":149.00,"currency":"USD","wly":"abc123"}' SIG=$(printf "%s.%s" "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed "s/.*= //") curl -X POST "https://your-domain.com/api/track/conversion?key=...&secret=..." \ -H "Content-Type: application/json" \ -H "X-Wooly-Signature: t=$T,v1=$SIG" \ -d "$BODY"

Right and wrong

Right: body = JSON.stringify(payload) → sign body → post body.

Wrong: sign JSON.stringify(payload), then let your HTTP library serialise payload again. The second serialisation can reorder keys or change spacing, the bytes differ by one character, and the signature will never match. It fails on every single call and looks like a wrong secret.

If your platform cannot add a header at all, the panel's last example signs a fixed list of fields instead. Use it only if the others are genuinely impossible.


Step 4: Send a test event

Step 4 in the panel gives you a ready-made signed call that cannot cost anything. It sends a custom event with no amount and no click reference, so there is nothing to credit and nothing to pay:

BODY='{"event":"wooly_test","orderId":"wooly-test-1"}'

It proves the three things that actually go wrong: the address is right, the secret is right, and your signature covers the exact bytes you sent. It is also what unlocks campaign creation on this account.

Two notes. The switch has to be on first, a closed door refuses everything, including a test. And change the orderId if you send it twice; the same reference twice comes back as duplicate, which is the retry protection working, not a failure.


Every status and every reply, explained

The badges on the section header and every reply the address can give, with what each one means

The badges on the section header

Notices inside the section

“Switched off.”
The address refuses everything. Nothing else on the page depends on this, so if you never turn it on, nothing is missing.

“Your tracking key was rotated, and nothing has reached this address since.”
The rotation replaced this address too, so your backend is posting to one that no longer exists and is being refused in silence. Copy the address in step 2 again and paste it into your backend. Sales already recorded are untouched and every commission they earned stands; what stops is anything your server has tried to report since.

“Your sales are arriving, but none of them name a creator.”
Shown when a long run of reported sales has credited nobody; no click reference, no coupon, nothing. Some sales genuinely have no creator behind them, but a whole run without a single one usually means the wly field is not being filled in. Go back to step 1. Nothing is lost: those sales are recorded and your totals are right. What is missing is the commission each one would have paid.

What the address replies

Unlike the tag, this address talks back, so a mistake shows up in your own logs instead of going quiet for a month.

Every rejection records nothing at all. There is no half-written sale to clean up.

Custom event names are the one exception to de-duplication. Sales and signups are matched on their reference, so a repeat is recognised. Your own event names have nothing to match on, so every delivery is stored, and a webhook queue that retries for hours will store one row each time. It cannot pay anything or move a balance, but it will inflate your counts and your activity feed. Fire custom events from somewhere that sends once.


Worked examples

A subscription that renews monthly

A customer signs up through a creator link in January. Your checkout page's tag reports the first payment; you also save wly=abc123 on their subscription record. Every renewal after that happens with no browser anywhere, so your billing job posts a signed conversion with a fresh orderId (#SUB-1001-FEB, #SUB-1001-MAR) and the same wly. Each renewal credits the same creator, and no renewal collides with another.

A payment captured the next day

An order is authorised at checkout and captured 18 hours later by your payment provider's webhook. Your tag never sees the capture. Your server reports it with orderId: "#1001"; the same order number the tag would have used. If your tag did already report #1001, the reply comes back as a duplicate and nothing is double-counted. That is the safe outcome, and it is why the order number matters.

A phone order

No browser, no tag, no click reference, the customer called you. Your server reports it with an orderId and an amount, and no wly. The reply says recorded and credited nobody. That is exactly right: nobody sent this customer.


Troubleshooting

Do you have to remove the tag?

No, and usually you should not. Removing it turns off exactly two things (the tag test, and the alert that tells you your site has gone quiet), because both read events from a browser specifically. Nothing else changes: sales are recorded, attributed and paid exactly as before, the badge at the top of the page reads connected via your server, and campaign creation stays open. One proved route is all the account is ever asked for.

A short checklist

Related reading

More from the Wooly Network Knowledge Library

Follow Wooly Network

Wooly Network