Skip to content

Stripe, Paddle and Lemon Squeezy

The same idea as WooCommerce for shops that sell with a payment platform: a payment event arrives, nimdeploy checks its signature and runs your provisioning script with the event in a file.

[deploy.checkout]
provider = "stripe"                     # or paddle, lemonsqueezy
path = "/hooks/stripe"
secret_env = "STRIPE_WEBHOOK_SECRET"    # whsec_... / pdl_ntfset_... / the signing secret
events = ["checkout.session.completed", "invoice.paid"]
command = "/home/deploy/bin/provision.sh"
timeout = "30m"

# Optional: validated values for the script
[deploy.checkout.params]
PLAN = { from = "data.object.metadata.plan", enum = ["basic", "pro"] }
Provider Create the webhook in Signature Event type → DEPLOY_EVENT Object → DEPLOY_RESOURCE_ID
stripe Developers → Webhooks → Add endpoint Stripe-Signature (HMAC of t.body, ±5 min) type data.object.id
paddle (Billing) Developer tools → Notifications → New destination Paddle-Signature (HMAC of ts:body, ±5 min) event_type data.id
lemonsqueezy Settings → Webhooks X-Signature (HMAC of the body) meta.event_name data.id

The URL is https://<your domain>/hooks/<path> and the secret goes in secrets.env under secret_env. With signatures that carry a timestamp (Stripe, Paddle), deliveries older than max_skew (default 5m) are refused.

What your script gets

DEPLOY_EVENT, DEPLOY_RESOURCE_ID, DEPLOY_PAYLOAD_FILE (the whole event, 600, deleted after the run) and your params. For example, with Stripe Checkout:

session=$DEPLOY_RESOURCE_ID
customer_email=$(jq -r '.data.object.customer_details.email' "$DEPLOY_PAYLOAD_FILE")
plan=$(jq -r '.data.object.metadata.plan' "$DEPLOY_PAYLOAD_FILE")

Put what you need to provision (plan, account ID…) in the checkout's metadata (Stripe), custom_data (Paddle) or checkout_data.custom (Lemon Squeezy, arrives as meta.custom_data).

Delivery rules

These platforms retry failed deliveries (Stripe for up to 3 days, Lemon Squeezy 3 times), so nimdeploy answers accordingly:

Situation Answer Why
bad or old signature 401 not from the platform
event type not in events, when not met 200 ignored nothing to do
invalid params, broken JSON 200 rejected + log + notification retrying would fail the same way
same event again (event ID, or body hash for Lemon Squeezy) 200 duplicate already queued or run
queue full (queue_max) 503 the platform retries later
accepted 202 queued on disk, in order (queue_mode = "all" by default)

As with WooCommerce, make the script idempotent (keyed by the session, transaction or order ID): retries and replays from the platform's dashboard must not provision twice.