Skip to main content

Overview

dynamic_webhook is not a normal Shoppex event webhook. It is a direct server-to-server callback that Shoppex sends when a DYNAMIC product is being fulfilled after a paid order. Here’s how it works:
1

Create the product

You create a product with type: "DYNAMIC", set dynamic_webhook, and keep the generated dynamic webhook signing secret.
2

Customer pays

A customer pays for that product.
3

Shoppex calls your endpoint

Shoppex sends POST to your dynamic_webhook URL.
4

Your server responds

Your server returns the dynamic delivery data.
5

Shoppex stores the result

Shoppex stores that response in the delivered items for the invoice.
This page covers the callback contract for dynamic_webhook. For normal Shoppex event webhooks like order:paid, see Webhooks Overview and Webhook Events.

When Shoppex Calls It

Shoppex calls the dynamic_webhook URL during product fulfillment after the invoice reaches a paid/completed state. The customer buys your dynamic product, Shoppex marks the invoice as paid, starts fulfillment, calls your endpoint, and saves your response into the invoice delivery details.

Request

Shoppex sends:
  • Method: POST
  • Content-Type: application/json
  • Body: JSON payload with invoice, product, shop, and line item data

Headers

Signing Secret

Dynamic Product Delivery has its own signing secret on the product. It is separate from normal Shoppex event webhook secrets.
  • Normal event webhooks: create the endpoint in Settings -> Webhooks and use that endpoint secret.
  • Dynamic product delivery: set dynamic_webhook on the product and use the product’s dynamic webhook signing secret.
When creating or updating a dynamic product through the Developer API, pass dynamic_webhook_secret to set your own secret. If you set dynamic_webhook without a secret, Shoppex generates one and returns it in dynamic_webhook_secret in that create or update response. Store it immediately. If the product has a signing secret, Shoppex signs ${deliveryId}.${timestamp}.${rawBody} and sends the digest in X-Shoppex-Signature-V2. Reject timestamps outside a 5-minute window. Simple example:
X-Shoppex-Signature is retained as a legacy HMAC-SHA512 body-only header during migration. New dynamic delivery handlers should verify X-Shoppex-Signature-V2.
Treat X-Shoppex-Idempotency-Key as the durable fulfillment key. If the first request times out and Shoppex retries, both requests carry the same idempotency key — your server should return the same result instead of issuing a second token or license. This is the most common source of bugs in dynamic delivery integrations.

Payload Shape

The payload contains both camelCase and snake_case for the most important fields. This is intentional so simple handlers do not need a translation layer first.

Top-Level Example

Important Fields

Response

Your endpoint should return 2xx and JSON. Recommended response:

What Shoppex Accepts

Shoppex accepts these response forms:
  • A JSON object
  • A JSON object with a nested data object
  • A non-empty string
If you return a JSON object with data, Shoppex stores the nested data object.

What Shoppex Stores

Shoppex normalizes your response into delivered items:
If you return a plain string, Shoppex stores it as dynamic_response. If you return an empty body, Shoppex stores a fallback message and count: 0.

Retry and Timeout Behavior

Shoppex retries only transient failures:
  • Timeout: 15 seconds
  • Retry delays: 1s, then 3s
  • Retry triggers: timeout, 429, 500, 502, 503, 504, and common network errors
So if your server returns 503, Shoppex retries after 1s. If that also fails, it retries once more after 3s. After the third attempt, the delivery is marked as failed.
If all retries fail, the customer sees a generic “delivery pending” message on their order page. The order stays in a fulfilled-but-undelivered state until you manually resolve it or the customer contacts support.

URL Requirements

Your dynamic_webhook must be a valid public http or https URL. For local development, use a tunnel such as ngrok or Cloudflare Tunnel.
  • https://dev.example.com/shoppex/dynamic — works
  • https://abc123.ngrok.io/shoppex/dynamic — works for local testing
  • http://127.0.0.1:3000/... — won’t work, Shoppex can’t reach private/loopback URLs

Example Handler

Here’s what a solid integration looks like:
  • use idempotencyKey as your fulfillment key
  • return the same result for retries
  • keep the response short and structured
  • put the customer-facing text in service_text
  • put machine-readable output like tokens or credentials in dynamic_response
Avoid:
  • generating a new token on every retry
  • depending on field names from only one casing style
  • returning HTML or large non-JSON payloads

Next Steps

Webhooks Overview

Setup, signatures, and retry policies

Webhook Events

Full event type reference and payload schemas