When a winner pays (or wins a pair for pickup), ENTD creates the order in your shop — paid, with the customer and shipping address — so you fulfil it like any other order. Webhooks tell your own systems about every step of the draw.
In the ENTD admin panel open Integrations → Commerce connections, choose the platform and paste the key: a Shopify custom app access token with write_orders, a WooCommerce consumer key and secret (Read/Write), or a Spree secret API key with write_orders and write_payments. Press Check connection.
On each drawing, set “Shop variants”: which variant in your shop each option (size, color / size, volume…) corresponds to; several levels are joined with “ / ”. Then orders take the right item and your stock goes down. Without a mapping Shopify and WooCommerce get an item with the release name and the chosen option; Spree needs the mapping.
{
"US 9": "44117263155455",
"US 9.5": "44117263188223",
"US 10": "44117263220991"
}In Integrations → Webhook endpoints add your HTTPS URL and pick the events (none selected — all events). Copy the secret and use Send test event to check the setup.
Each request is signed with your secret. Reject requests with a wrong signature or an old timestamp, answer with any 2xx status quickly and do the work in the background.
// Node.js (Express): use the raw request body, not the parsed JSON
import crypto from 'node:crypto'
app.post('/entd-webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-ENTD-Signature') // t=1730000000,v1=5257a8…
const { t, v1 } = Object.fromEntries(header.split(',').map(part => part.split('=')))
const expected = crypto
.createHmac('sha256', process.env.ENTD_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.sendStatus(400)
}
const event = JSON.parse(req.body)
// event.id is the same for every retry — skip ids you have already handled
res.sendStatus(200)
})participation.won — the participant won (or got the pair after another winner didn’t pay)participation.paid — the winner paid, including automatic charges of a saved cardparticipation.forfeited — the payment window ended without payment, the pair goes to the next participantparticipation.shipped — the UPS label is created, the tracking number is in the payloadorganization_drawing.finished — the draw is done{
"id": "2f6c1b7e-5f0a-4c34-9d3e-8b1e6a7c9d10",
"type": "participation.paid",
"created_at": "2026-10-29T10:15:00Z",
"data": {
"participation": {
"id": "8c0d…",
"customer": { "email": "[email protected]", "name": "Jane Doe", "phone": "+33612345678" },
"desired_item": ["US 9"],
"variant": "4242",
"is_winner": true,
"paid": true,
"shipping_address": { "address1": "1 Rue de Rivoli", "city": "Paris", "country_code": "FR", "zip_code": "75001" },
"tracking_number": null
},
"organization_drawing": { "id": "b51e…", "release_name": "Air Jordan 1 High", "price": "180.0", "currency": "EUR" }
}
}Failed deliveries (no answer within 10 seconds or a non-2xx status) are retried with growing pauses for about a day. The event id stays the same, so handle each id once. Every delivery and its response are listed on the endpoint page, with a button to send it again.