Skip to content

Developer docs

Dynamic delivery

Set a product's delivery type to dynamic and paste your URL. On every paid order Hidden POSTs the signed order and delivers the deliveredData string you answer with. Use it to create accounts, generate keys or call your own licence server.

How it works

The request has the same format as a webhook. Your JSON answer's deliveredData is what the buyer receives.

your-endpoint (response)http
// Your endpoint receives the same signed payload as a webhook.
// Respond within 10 seconds with what the buyer should receive:
HTTP/1.1 200 OK
Content-Type: application/json

{
  "deliveredData": "Your licence: HDN-4K2P-88QL-9XQ1\nActivate at https://example.com/activate",
  "note": "Shown to the buyer under the item (optional)"
}

// Anything other than 2xx (or a timeout) marks the item "failed" with your response
// body as the error. You can redeliver from the order page in the dashboard.

// Your own supplier account cannot pay for this order? Answer 200 with
//   { "status": "failed", "code": "insufficient_balance", "error": "Balance too low" }
// The item then waits for you instead of failing: the buyer is told it is on its way,
// you get an alert, and Redeliver sends it once you have topped up.

Try it before you sell. In the product editor, Send test request POSTs a signed sample order to your address ("test": true in the body and an X-Hidden-Test: 1 header). No order is created and nobody is charged. The editor then shows what a real order would do with your answer.

"test": trueX-Hidden-Test: 1

Checking the signature

Every request carries X-Hidden-Timestamp (unix seconds) and X-Hidden-Signature (sha256= + HMAC-SHA256 of ${timestamp}.${rawBody} with the product's secret from the editor). Compute the same HMAC over the raw body and compare in constant time, rejecting timestamps older than a few minutes.

  1. 1Signed string
    X-Hidden-Timestamp . rawBody

    The timestamp header, a dot, the raw request body.

  2. 2HMAC-SHA256
    with your secret

    Digest the string with the endpoint secret.

  3. 3Compare
    in constant time

    Against X-Hidden-Signature after sha256=.

import { createHmac, timingSafeEqual } from "node:crypto";

// Use the raw request body exactly as received (do not JSON.parse first).
export function verifyHiddenSignature(rawBody: string, headers: Headers, secret: string) {
  const header = headers.get("x-hidden-signature") ?? "";      // "sha256=<hex>"
  const timestamp = headers.get("x-hidden-timestamp") ?? "";   // unix seconds

  // Reject stale deliveries (replay protection), 5 minute tolerance.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const given = header.replace(/^sha256=/, "");
  if (given.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex"));
}

// Next.js route handler
export async function POST(req: Request) {
  const raw = await req.text();
  if (!verifyHiddenSignature(raw, req.headers, process.env.HIDDEN_WEBHOOK_SECRET!)) {
    return new Response("invalid signature", { status: 401 });
  }
  const event = JSON.parse(raw);
  // De-duplicate on the X-Hidden-Delivery header before acting.
  // ...handle event.data
  return new Response("ok");
}
import hmac, hashlib, time

def verify_hidden_signature(raw_body: bytes, headers: dict, secret: str) -> bool:
    given = headers.get("x-hidden-signature", "").removeprefix("sha256=")
    ts = headers.get("x-hidden-timestamp", "0")
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(given, expected)
<?php
// Use the raw request body exactly as received (do not json_decode first).
$raw       = file_get_contents("php://input");
$given     = substr($_SERVER["HTTP_X_HIDDEN_SIGNATURE"] ?? "", strlen("sha256="));
$timestamp = $_SERVER["HTTP_X_HIDDEN_TIMESTAMP"] ?? "0";

if (abs(time() - (int) $timestamp) > 300) { http_response_code(401); exit; } // stale: replay protection

$expected = hash_hmac("sha256", $timestamp . "." . $raw, getenv("HIDDEN_SECRET"));
if (!hash_equals($expected, $given)) { http_response_code(401); exit; }

$order = json_decode($raw, true);
if (!empty($order["test"])) { /* a test from the product editor: nothing was paid */ }

header("Content-Type: application/json");
echo json_encode(["deliveredData" => "Your licence: HDN-4K2P-88QL-9XQ1"]);

Shown in Node

Always verify the signature

Anyone who learns your endpoint URL could POST to it. Verify the signature before provisioning anything.

Get help

Send us the endpoint, the request or delivery id and the response you got. Security findings go through responsible disclosure. Live platform status is on the support page.

See also