Skip to main content
dynamic_webhook is a direct server-to-server callback that Shoppex sends when a DYNAMIC product needs fulfillment after a paid order, separate from normal event webhooks.
This page covers the callback contract for dynamic_webhook. For normal Shoppex event webhooks like order:paid, see Webhooks and Webhook events.

How it works

  1. You create a product with type: "DYNAMIC", set dynamic_webhook, and keep the generated dynamic webhook signing secret.
  2. A customer pays for that product.
  3. Shoppex sends POST to your dynamic_webhook URL.
  4. Your server returns delivery data, or clearly states that it will fulfill the line item later.
  5. Shoppex either stores the delivered content or marks the line item as AWAITING_FULFILLMENT.

Webhook request

HTTP details

Shoppex sends a POST request with Content-Type: application/json and a JSON body that contains invoice, product, shop, and line item data. Your dynamic_webhook URL must be a public http or https address. 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/... — does not work. Shoppex cannot reach private or loopback URLs

Example body

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

Request headers

Signature verification

Dynamic product delivery has its own signing secret on the product. This secret is separate from normal Shoppex event webhook secrets: normal webhooks use the endpoint secret from Settings → Webhooks, dynamic delivery uses the product’s dynamic_webhook_secret. When you create or update 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 once as dynamic_webhook_secret in that create or update response. Store it immediately. If the product has a signing secret, Shoppex signs ${deliveryId}.${timestamp}.${rawBody} with HMAC-SHA256 and sends the digest in X-Shoppex-Signature-V2. Reject timestamps outside a 5-minute window.
Shoppex keeps X-Shoppex-Signature as a legacy HMAC-SHA512 body-only header during the migration period. New dynamic delivery handlers must verify X-Shoppex-Signature-V2.
Treat X-Shoppex-Idempotency-Key as the durable fulfillment key for this callback. Shoppex makes only one automatic request, but a merchant can retry a failed delivery manually. Your endpoint must return the same result for the same key instead of issuing a second token, license, or account.

Webhook response

Success

Your endpoint must return 2xx status and JSON. Recommended response:
A solid integration follows this pattern:
  • Use idempotencyKey as your fulfillment key.
  • Return the same result if a merchant retries a failed delivery.
  • 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 the following:
  • Generating a new token when Shoppex sends the same idempotency key again.
  • Depending on field names from only one casing style.
  • Returning HTML or a large non-JSON payload.
Shoppex accepts three response forms: a JSON object, a JSON object with a nested data object, or a non-empty string. If you return a JSON object with data, Shoppex stores the nested data object:
Shoppex normalizes your response into delivered items:
If you return a plain string, Shoppex stores it as dynamic_response. If your system accepts the request but cannot return the delivery content within 15 seconds, respond with 200 and a clear pending status. Shoppex then marks this line item as AWAITING_FULFILLMENT. Deliver the content later with POST /dev/v1/orders/{id}/items/{item_id}/fulfill. The following example shows a complete callback exchange. The request body uses the same fields described above. The response is the asynchronous acknowledgement.
The nested form is also accepted:

Example handler

Retryable errors

Shoppex makes one delivery attempt with a 15-second timeout. It does not retry automatically after a timeout, network error, 429, or 5xx response. This avoids duplicating a side effect when Shoppex cannot know whether your server already delivered the product before the connection failed. After this terminal failure, Shoppex:
  • marks the line item as FAILED
  • notifies the merchant
  • sends order:item.delivery_failed to subscribed merchant webhook endpoints
The merchant can fulfill the failed line item manually through the dashboard or the Developer API, or retry the dynamic delivery again. A timeout or network error does not tell you whether the vendor already delivered the product. Check X-Shoppex-Idempotency-Key for duplicates before you take any action with a side effect.

Non-retryable errors

You must state pending clearly. An empty 2xx response keeps its historical meaning: Shoppex treats the line item as delivered and stores a placeholder delivery note. Never use an empty body to say that you will deliver the content later.
An empty or accidentally empty 2xx response has no retry path. Shoppex already considers the item delivered, so it does not call your endpoint again for that line item. Always send the pending response shown above when you cannot deliver content immediately.

Invoice status behavior

Shoppex calls the dynamic_webhook URL during product fulfillment, after the invoice reaches a paid or completed state. A 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.

Webhooks

Setup, signatures, and retry policies

Webhook events

Full event type reference and payload schemas