Order Webhook (Generic)
If you're not on Shopify, this is how you tell SimplyClub that an order was paid or refunded. It's a single JSON endpoint that any backend can call — WordPress, WooCommerce, custom commerce, mobile apps, or your own ERP. The contract is plain HTTP + JSON; there's nothing language-specific about it.
Just like the Shopify webhook, this endpoint is what finalizes the loyalty side of an order: paid orders accrue points and lock in the chosen benefit; refunds reverse them. Without it, your customers see benefits in the cart but nothing gets recorded on the SimplyClub side after checkout.
Endpoint
POST https://dropins.simplyclub.co.il/api/v1/transaction/order-webhook
Content-Type: application/json
There are no required HTTP headers beyond Content-Type. The endpoint identifies and authenticates each call by the sc_transaction_code in the body — this is the session-bound token your store received from the SimplyClub drop-in during checkout. Treat it like a one-shot credential.
When to call it
| Your platform event | Topic to send |
|---|---|
| Order finalized / payment captured | orders/paid |
| Refund issued (full or partial) | refunds/create |
No
orders/cancelled. This endpoint does not have a cancellation topic. Cancellations that produce a refund should be sent asrefunds/create. Cancellations that don't reverse money have no loyalty impact, so no webhook is needed.
Request body
All fields below are top-level keys on the JSON body.
| Field | Type | Required for | Description |
|---|---|---|---|
topic |
string | all calls | One of orders/paid, refunds/create |
domain |
string | all calls | Your store domain — must match the domain configured on your POI |
sc_transaction_code |
string | all calls | The transaction code SimplyClub issued during this checkout's session |
order_id |
string | number | all calls | Your platform's order ID |
customer_id |
string | number | orders/paid |
Your platform's customer ID. Not required for refunds/create — the session is resolved by order_id. |
order_number |
string | number | optional | Human-readable order number, surfaced in logs and on the SimplyClub transaction |
total_price |
string | orders/paid |
Order grand total as a decimal string (e.g. "150.00"). Stored on the session for refund math. |
items |
array | orders/paid |
Line items. See Item shape below. (Accepted alias: line_items.) |
discount_applications |
array | orders/paid |
Discount applications referenced by index from each item's discount_allocations. Send [] if none. |
Item shape
Each entry in items is an object:
| Field | Type | Description |
|---|---|---|
id |
string | number | Line item ID on your platform |
title |
string | Product title shown to the customer |
quantity |
number | Quantity purchased |
price |
string | number | Unit price (decimal string is fine) |
sku |
string | SKU. If missing, variant_id is used as a fallback. |
discount_allocations |
array | Per-line discount allocations, each { "amount": "...", "discount_application_index": <int> }. Send [] if none. |
Example: paid order
This is the full JSON body for a single paid order with one line and no discounts. Copy it, change the values, and post it as-is from any language.
{
"topic": "orders/paid",
"domain": "simply-club-demo-store-01.myshopify.com",
"sc_transaction_code": "18202406",
"order_id": "6402739798187",
"customer_id": "8551254393003",
"order_number": "1015",
"total_price": "150.00",
"items": [
{
"id": 16810353229995,
"title": "סינודא טרשיט",
"quantity": 1,
"price": "150.00",
"sku": "1008",
"discount_allocations": []
}
],
"discount_applications": []
}
Example: refund
Refunds only need to identify the session — order_id and the token are enough. SimplyClub uses the totals it stored at paid time to reverse the loyalty side.
{
"topic": "refunds/create",
"domain": "simply-club-demo-store-01.myshopify.com",
"sc_transaction_code": "18202406",
"order_id": "6402739798187"
}
Where sc_transaction_code comes from
The drop-in issues it during the customer's session. Your storefront receives it via the transactionCodeCreated event and must persist it onto the order before checkout completes, so your backend can include it in this webhook later.
Typical flow:
- Customer opens the SimplyClub drop-in on a product or cart page.
- Drop-in fires
transactionCodeCreatedwith a string token. - Your storefront stores the token against the cart/session (e.g. cart attribute, session cookie, order meta).
- On checkout, your backend persists the token onto the order record.
- When the order's status changes (paid / refunded), your backend calls this webhook with that token.
If sc_transaction_code is missing from the webhook, SimplyClub cannot link the event to a session and rejects the call.
Responses
Every response is JSON wrapped in { "success": <bool>, "data": <object>, "error": <object|null> }. HTTP status codes:
| Status | Meaning |
|---|---|
200 |
Webhook accepted. Check success in the body — true means the loyalty transaction completed; false means SimplyClub received and recorded the call but the downstream POS transaction failed (see errorCode / errorMessage). If data.skipped is true, the topic was acknowledged but disabled for this POI. |
400 |
Validation failed. The response body lists what's wrong: missing required fields, unresolvable sc_transaction_code, or no POI matching the domain. |
404 |
The topic is not handled (anything other than orders/paid or refunds/create after delegation). |
500 |
Internal processing error. The full error is in SimplyClub's logs — contact support with your sc_transaction_code and order_id. |
Example: success
{
"success": true,
"data": {
"message": "Transaction completed",
"transactionCode": "18202406",
"sessionId": "6a03530bdc0698c97bfb0bca",
"customer": {
"clubId": "2886",
"cardNumber": "2886100208"
}
}
}
Example: topic disabled (skipped)
{
"success": true,
"data": {
"message": "Webhook topic \"refunds/create\" is disabled",
"skipped": true
}
}
Example: duplicate transaction error
If you re-deliver the same sc_transaction_code after the POS has already accepted it, SimplyClub returns HTTP 200 with success: false and the POS error attached. This is the most common production error — treat duplicates as already-processed and stop retrying.
{
"success": false,
"data": {
"message": "Transaction failed",
"processName": "Transaction/TranEndCheck",
"errorCode": 187,
"errorMessage": "DuplicatePOSTransactionNumber",
"transactionResponse": {
"TranReferenceNumber": "",
"Vouchers": [],
"ReceiptInfo": [],
"PromotedItems": [],
"ErrorPromos": [],
"ErrorPromosNames": [],
"ErrorCode": 187,
"ErrorMessage": "DuplicatePOSTransactionNumber",
"AdditionalInfo": null
},
"transactionCode": "18202406",
"sessionId": "6a03530bdc0698c97bfb0bca",
"customer": {
"clubId": "2886",
"cardNumber": "2886100208"
},
"updates": {
"order": {
"tags": { "add": [], "remove": [] },
"metafields": {
"set": [
{
"namespace": "sc_custom",
"key": "sc_error_message",
"value": "2026-05-21T00:17:19.741Z - 187 - DuplicatePOSTransactionNumber",
"type": "multi_line_text_field"
}
]
}
},
"customer": {
"tags": { "add": [], "remove": [] }
}
}
}
}
The updates block tells your storefront what to mirror back to the order (tags and metafields to apply). It's optional to act on, but it's what the Shopify drop-in writes to the order so support can see why a transaction failed.
Example: missing required field
{
"success": false,
"data": {
"message": "Missing required fields",
"missing": ["sc_transaction_code"]
},
"error": null
}
Per-POI topic toggles
Even when the webhook is accepted, SimplyClub only acts on topics that are enabled for your POI:
orders/paid— on by defaultrefunds/create— off by default
If a topic is disabled, the endpoint returns 200 with data.skipped: true and takes no action. Toggle topics from the SimplyClub dashboard.
Reliability notes
- Retry on network errors and
5xx. Use exponential backoff. The endpoint is idempotent onwebhookRecordcreation — duplicate deliveries for the same order/topic are safe at the SimplyClub layer. - Do not retry on
200witherrorCode: 187(DuplicatePOSTransactionNumber). The POS has already accepted that transaction. Retrying will just produce the same error. - Do not retry on
400. Fix the payload and re-send. - Fire-and-forget is fine. SimplyClub processes the webhook synchronously and returns when finished, but you don't need to block the user-facing order flow on the response.
- One call per event. Don't merge multiple status changes into one webhook — call once per
orders/paidand once perrefunds/create.
Common rejection reasons
| Response | Cause | Fix |
|---|---|---|
400 Missing required fields (lists fields) |
One of topic, domain, sc_transaction_code, order_id is absent or empty. |
Send all four. Empty strings count as missing. |
400 Missing required field: customer_id |
orders/paid was sent without customer_id. |
Always include customer_id for paid orders. |
400 POI not found |
The session resolves to a poiId that doesn't exist, or your domain doesn't map to one. |
Confirm your store domain matches what's onboarded with SimplyClub. |
200 with data.skipped: true |
The topic is disabled for this POI. | Enable the topic from the SimplyClub dashboard. |
200 with success: false, errorCode: 187 |
Same sc_transaction_code already finalized on the POS — duplicate delivery. |
Stop retrying. Mark the order as already processed locally. |
Session can't be resolved (sc_transaction_code) |
The token doesn't match a known session, or was already finalized. | Make sure your storefront stores the token issued by the drop-in and forwards it to the webhook. |
Related
- Shopify Order Webhook — the Shopify-specific endpoint with header-based topic/domain and HMAC signing.
- Install on Shopify — how
sc_transaction_codeis wired into cart attributes on Shopify. - Membership CTA Web Component — embedding the join CTA on any platform.