Dock

All guides · Widget Guide

For developers

Connecting the widget to your own checkout

Works with any server-side stack · worked example in PHP, with Node, Next.js, Python and WordPress equivalents below

The Dock widget handles configuration, live pricing and artwork upload entirely on its own. What it deliberately does not do is take payment or create orders — your site owns the basket, the checkout and the customer's money. Connecting the two is a small, well-bounded job: one browser event listener and two server-to-server calls.

The three credentials — and which one is secret

Everything you need is in the merchant's Dock workspace under Settings → Widget Integration:

  • Tenant ID and API Key — already in the embed snippet. Safe in page code: they can only read the catalogue, quote prices, upload artwork and check an order's status by its reference, and they're rate-limited per account.
  • Server Secret — creates and confirms orders. Keep it in server-side configuration only (an environment variable or a config file outside the web root). It must never appear in HTML, JavaScript or a browser request. If it's ever exposed, email us and we'll rotate it immediately.

The flow at a glance

  1. The customer configures a product, sees the live price and uploads artwork — all inside the widget, all hosted by Dock.
  2. They click Add to Basket. The widget fires a browser event, solopress:add-to-basket, carrying the full line item.
  3. Your JavaScript catches the event and stores the line in your basket (session, database — your call). The customer pays through your checkout with your payment provider; Dock is not involved in taking their money.
  4. When payment succeeds, your server creates the Dock order (Server Secret call #1)…
  5. …and immediately confirms the payment with your transaction reference (call #2).
  6. The order appears in the merchant's Dock workspace, goes through the standing review step, and is released to production. You can poll status or receive webhooks.

Step 1 — catch the Add to Basket event

The event is dispatched on the <solopress-configurator> element and bubbles all the way up (it crosses the widget's Shadow DOM), so a document-level listener works fine:

<script>
  document.addEventListener("solopress:add-to-basket", async (e) => {
    // e.detail = {
    //   product:    "Premium A5 Flyers",        // display name
    //   productId:  "0a9ab5b8-…",               // Dock product id — use this
    //   quantity:   500,
    //   price:      { net: 38.00, vat: 7.60, gross: 45.60 },
    //   config:     { size: "A5 (210 x 148 mm)", material: "170gsm Silk", … },
    //   artworkUrl: "https://…/design.pdf"      // already uploaded, or null
    // }
    await fetch("/dock-basket-add.php", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(e.detail),
    });
    window.location.href = "/basket";
  });
</script>

Store e.detail against the customer's session verbatim — you'll pass productId, quantity, config and artworkUrl straight through at order time. The artwork is already uploaded and checked; never re-upload it.

Step 2 — create the order after your checkout succeeds (PHP example)

Server-side only. One call per completed checkout; put every basket line in the items array. The endpoint, headers and body are identical in every stack — Node, Next.js, Python and WordPress equivalents below.

<?php
// DOCK_TENANT_ID / DOCK_SERVER_SECRET from server-side config —
// values are in the Dock workspace, Settings → Widget Integration.
$line = $basketLine; // the stored e.detail from step 1

$payload = [
  "items" => [[
    "product"       => $line["productId"],
    "quantity"      => (int) $line["quantity"],
    "configuration" => $line["config"],
    "artworkUrl"    => $line["artworkUrl"],
    "deliveryAddress" => [
      "name"           => $customerName,
      "addressLine1"   => $address1,
      "city"           => $city,
      "postcode"       => $postcode,
      "isoCountryCode" => "GB",
    ],
  ]],
  "customerRef" => $yourOrderNumber,      // your own order reference
  "customerLegSettledExternally" => true,  // you took the customer's payment
];

$response = file_get_contents(
  "https://app.mydock.io/api/external/v1/orders",
  false,
  stream_context_create(["http" => [
    "method"        => "POST",
    "ignore_errors" => true, // read the response body on 4xx too
    "header"        => implode("\r\n", [
      "Content-Type: application/json",
      "X-Tenant-ID: " . DOCK_TENANT_ID,
      "X-Server-Secret: " . DOCK_SERVER_SECRET,
      "Idempotency-Key: order-" . $yourOrderNumber,
    ]),
    "content" => json_encode($payload),
  ]]),
);
$order = json_decode($response, true);

// $order["hubReference"]  e.g. "HUB-MNHK1RX2-HAPL" — store it with your order.
// $order["totalPrice"]    { net, vat, gross } — Dock's authoritative price
//                         (null if pricing was deferred; see Testing safely).
// $order["delivery"]      { date, orderBy } — estimated delivery + cut-off.
  • Idempotency-Key makes retries safe: any stable unique value (your order number works well). Re-sending the same key and body within 24 hours returns the original response — no duplicate order.
  • Multiple basket lines = multiple entries in items. Each item can carry its own deliveryAddress.
  • The response's totalPrice is computed by Dock and will match what the widget quoted for the same configuration (quotes can shift if a dispatch cut-off passes between quoting and ordering — treat the order response as authoritative).
  • Charging the customer long after the widget quoted (an overnight basket)? Re-quote server-side first — the same price call the widget makes, POST /api/external/v1/price with the stored product/configuration/quantity (the public key pair is enough) — or accept that a passed cut-off may move the total slightly. Otherwise the difference is silently the merchant's.

Step 3 — confirm the payment

Tells Dock the customer has paid on your side. Send it straight after the create call:

POST https://app.mydock.io/api/external/v1/orders/{hubReference}/confirm
X-Tenant-ID:     …same headers as step 2…
X-Server-Secret: …

{
  "paymentReference": "txn_8827431",   // your gateway's transaction id
  "paymentMethod":    "stripe",         // free text: "stripe", "worldpay", …
  "amount":           45.60,            // what the customer paid, GBP
  "customerLegSettledExternally": true
}

Send customerLegSettledExternally: true on the first confirm. It tells Dock that you collected the customer's money, so production costs are billed to the merchant's Dock account (card on file or account terms). Confirm is idempotent — a repeat call replays the original response — but a later call can't add the marker retroactively, so it belongs on the first one.

Other stacks — the same two calls

Nothing above is PHP-specific: the browser listener from step 1 is plain DOM and works unchanged on any site, and steps 2–3 are ordinary JSON-over-HTTPS. Compact equivalents of the create call:

Node.js (18+, built-in fetch — Express, Fastify, anything)

const res = await fetch("https://app.mydock.io/api/external/v1/orders", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Tenant-ID": process.env.DOCK_TENANT_ID,
    "X-Server-Secret": process.env.DOCK_SERVER_SECRET,
    "Idempotency-Key": "order-" + yourOrderNumber,
  },
  body: JSON.stringify(payload),
});
const order = await res.json();

Next.js / React sites

Two notes. First, the widget is a plain custom element and widget.js is an ES module — keep the type="module" attribute when loading it (with next/script, pass type="module"; a bare script tag in your layout works too). Attach the listener in a useEffect, because solopress:add-to-basket is a DOM event, not a React synthetic one:

useEffect(() => {
  const onAdd = (e) =>
    fetch("/api/basket", { method: "POST", body: JSON.stringify(e.detail) });
  document.addEventListener("solopress:add-to-basket", onAdd);
  return () => document.removeEventListener("solopress:add-to-basket", onAdd);
}, []);

Second, the create/confirm calls belong in a route handler (e.g. app/api/dock-order/route.ts) using the Node sample above — never in a client component, or the Server Secret ships to the browser.

Python

import os, requests

res = requests.post(
    "https://app.mydock.io/api/external/v1/orders",
    json=payload,
    headers={
        "X-Tenant-ID": os.environ["DOCK_TENANT_ID"],
        "X-Server-Secret": os.environ["DOCK_SERVER_SECRET"],
        "Idempotency-Key": f"order-{your_order_number}",
    },
    timeout=30,
)
order = res.json()

WordPress (custom site — not WooCommerce)

On WooCommerce? Stop here — the Dock for WooCommerce plugin already does this whole flow; don't build it by hand. For a custom WordPress site without Woo, use wp_remote_post from a small plugin or theme function, with the credentials as constants in wp-config.php:

$response = wp_remote_post("https://app.mydock.io/api/external/v1/orders", [
  "headers" => [
    "Content-Type"    => "application/json",
    "X-Tenant-ID"     => DOCK_TENANT_ID,
    "X-Server-Secret" => DOCK_SERVER_SECRET,
    "Idempotency-Key" => "order-" . $order_number,
  ],
  "body"    => wp_json_encode($payload),
  "timeout" => 30,
]);
$order = json_decode(wp_remote_retrieve_body($response), true);

Website builders & static sites (Wix, Squarespace, Webflow, plain HTML)

The embed and the step-1 listener work anywhere JavaScript runs — but these platforms give you nowhere safe to keep a Server Secret. Put steps 2–3 in a tiny serverless function (Cloudflare Workers, Netlify or Vercel functions — the Node sample above is the whole body) and have your page call that. It's ~20 lines; email us and we'll help you stand one up.

After that — review, production, tracking

The order now sits in the merchant's Dock workspace at the standing review step (orders skip it only if the account has auto-release switched on). Artwork has already been through Solopress prepress checks via the widget; anything flagged as blocking holds the order at release rather than being printed.

To follow progress from your site, either:

  • Poll: GET /api/external/v1/orders/{hubReference} with the same two headers — returns status, per-item state and tracking once dispatched.
  • Webhooks: set a Webhook URL + Secret in the Dock workspace (Settings → Webhooks) and Dock POSTs status events to you (approved, submitted, in_production, sent with tracking, on_hold, cancelled — plus failure events such as submission_failed, payment_failed and refund_issued on the same URL, so ignore event values you don't recognise rather than erroring). Verify every delivery before trusting it — the X-Soloflo-Signature header is a plain hex HMAC-SHA256 of the raw body:
    $body = file_get_contents("php://input");
    $sig  = $_SERVER["HTTP_X_SOLOFLO_SIGNATURE"] ?? "";
    if (!hash_equals(hash_hmac("sha256", $body, DOCK_WEBHOOK_SECRET), $sig)) {
      http_response_code(401); exit;
    }

Testing safely

Orders created this way land at the review step, and nothing is printed or billed until the merchant releases them — with one exception: if the account has auto-release switched on, a confirmed order submits to production automatically. So: use an obvious customerRef (e.g. TEST-…), don't approve it, and if auto-release is on for your account, skip the confirm call on test orders (or email us and we'll test alongside you). Email us to void the test order when you're done.

Two checks that catch most first-run problems: the create call returns 401 when the Tenant ID / Server Secret pair is wrong (re-copy from Settings). A broken configuration doesn't always error: when only some lines of a multi-line order fail to price you get a 422, but when every line fails — including a single-line order — the order is still created with totalPrice: null. Treat a null totalPrice as a configuration problem, and pass the widget's config object through untouched rather than rebuilding it.

We'll do this with you

This page covers everything a standard integration needs (the full API reference — the cancel endpoint, every webhook event, error shapes — is available on request and via the AI assistant in the Dock workspace). And you don't have to integrate alone — email soloflo@solopress.com and we'll wire it together with your developer, including a walked-through test order. The AI assistant inside the Dock workspace can also answer detailed API questions — it holds the full external API reference.