The curl extension, an idempotent order, a tracking link — nothing to install with Composer.
In the dashboard, open API keys and create a key in Test mode. It starts with ps_test_ — test keys are free, never charge a card, and never reach a print facility. You can create one before adding a payment method.
Create a test keyexport PRINTEDSIMPLY_API_KEY=ps_test_YOUR_KEYA live key (ps_live_) takes the same requests. A real order also needs a card on file and your own artwork: Going live, at the end of this page, has the four changes.
Every response is the same envelope: status, data, and request_id. Quote the request_id in any support thread and we can find the exact call. Each product's sku is what an order names. Save it as products.php and run php products.php. It needs only PHP's curl extension (php -m lists curl when it is there; on Debian and Ubuntu it is the php-curl package), so there is nothing to install with Composer (PHP 8.1 and later).
<?php
$apiKey = getenv('PRINTEDSIMPLY_API_KEY');
if (!$apiKey) {
fwrite(STDERR, "Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_...) first\n");
exit(1);
}
$ch = curl_init('https://portal.printedsimply.com/api/v1/products?limit=3');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["x-api-key: $apiKey"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
if ($raw === false) {
fwrite(STDERR, 'Request failed: ' . curl_error($ch) . "\n");
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$reply = json_decode($raw, true);
if ($status >= 400) {
$error = $reply['error'] ?? [];
fwrite(STDERR, sprintf(
"%d %s: %s (request_id %s)\n",
$status,
$error['code'] ?? 'error',
$error['message'] ?? $raw,
$reply['request_id'] ?? 'none'
));
exit(1);
}
$data = $reply['data'];
foreach ($data['products'] as $product) {
echo $product['sku'], ' - ', $product['name'], "\n";
}Rows carry more than these three fields, the price of one unit among them; POST /v1/orders/quote prices a whole order. The order below names the first row's sticker as sticker-die-cut-3x3: a SKU is matched whatever its case.
The order's reference doubles as its Idempotency-Key: send the same request again and you get this order back, never a second one; the same key with a different body is refused with 422. A new order needs a new reference as well as a new key: one reference is one order for 30 days, so the same reference under another key is refused with 409 duplicate_reference, naming the order that has it. Run it as you ran products.php: php order.php.
<?php
$apiKey = getenv('PRINTEDSIMPLY_API_KEY');
if (!$apiKey) {
fwrite(STDERR, "Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_...) first\n");
exit(1);
}
$order = <<<'JSON'
{
"items": [
{ "sku": "sticker-die-cut-3x3", "quantity": 100,
"artwork_url": "https://printedsimply.com/samples/sticker-3x3.png" }
],
"address_to": {
"name": "Jane Doe", "address1": "123 Main St",
"city": "Austin", "state": "TX", "zip": "78701", "country": "US"
},
"shipping": { "method": "standard" }
}
JSON;
$ch = curl_init('https://portal.printedsimply.com/api/v1/order');
curl_setopt_array($ch, [
CURLOPT_POSTFIELDS => $order,
CURLOPT_HTTPHEADER => [
"x-api-key: $apiKey",
'Content-Type: application/json',
'Idempotency-Key: order-' . bin2hex(random_bytes(8)),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
if ($raw === false) {
fwrite(STDERR, 'Request failed: ' . curl_error($ch) . "\n");
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$reply = json_decode($raw, true);
if ($status >= 400) {
$error = $reply['error'] ?? [];
fwrite(STDERR, sprintf(
"%d %s: %s (request_id %s)\n",
$status,
$error['code'] ?? 'error',
$error['message'] ?? $raw,
$reply['request_id'] ?? 'none'
));
exit(1);
}
$data = $reply['data'];
// $data['order_id'] is the order (test_ord_... on a test key); $data['tracking_url'] is its tracking page.
echo $data['order_id'], ' ', $data['charged'], "\n";
echo $data['tracking_url'], "\n";The requests use only the curl and json extensions, so the same calls work inside a WooCommerce plugin or any other PHP application; there, throw or log the error instead of exiting. The files here run from the command line so you can watch the order go through first.
Move the sandbox order straight to shipped instead of waiting for the schedule: php ship.php test_ord_… with the order_id order.php printed.
<?php
$apiKey = getenv('PRINTEDSIMPLY_API_KEY');
if (!$apiKey) {
fwrite(STDERR, "Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_...) first\n");
exit(1);
}
$orderId = $argv[1] ?? ''; // the order_id order.php printed: test_ord_...
$ch = curl_init("https://portal.printedsimply.com/api/v1/order/{$orderId}/simulate");
curl_setopt_array($ch, [
CURLOPT_POSTFIELDS => json_encode(['target_state' => 'shipped']),
CURLOPT_HTTPHEADER => ["x-api-key: $apiKey", 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
if ($raw === false) {
fwrite(STDERR, 'Request failed: ' . curl_error($ch) . "\n");
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$reply = json_decode($raw, true);
if ($status >= 400) {
$error = $reply['error'] ?? [];
fwrite(STDERR, sprintf(
"%d %s: %s (request_id %s)\n",
$status,
$error['code'] ?? 'error',
$error['message'] ?? $raw,
$reply['request_id'] ?? 'none'
));
exit(1);
}
$data = $reply['data'];
echo $data['order_id'], ' ', $data['status'], "\n";Left alone, a sandbox order moves on by itself: a scheduled job takes it to in_production, shipped and delivered over about five minutes, firing the same signed webhooks at each step. simulate does not wait for that schedule. It takes in_production, shipped, delivered, cancelled or issue_flagged, and only on a test key. An order the schedule has already delivered answers 400 already_terminal; to rehearse the step again, place a new order with a new reference and Idempotency-Key (the sample uses one value for both; the same key replays the delivered order, and the same reference under a new key is refused 409 duplicate_reference).
order.php printed the tracking link. Open it, then read the order: once it has shipped, tracking carries the sandbox tracking number (TRK-TEST-…) and its carrier. Simulate delivered, or wait, to watch it deliver.
<?php
$apiKey = getenv('PRINTEDSIMPLY_API_KEY');
if (!$apiKey) {
fwrite(STDERR, "Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_...) first\n");
exit(1);
}
$orderId = $argv[1] ?? '';
$ch = curl_init("https://portal.printedsimply.com/api/v1/order/{$orderId}");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["x-api-key: $apiKey"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
if ($raw === false) {
fwrite(STDERR, 'Request failed: ' . curl_error($ch) . "\n");
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$reply = json_decode($raw, true);
if ($status >= 400) {
$error = $reply['error'] ?? [];
fwrite(STDERR, sprintf(
"%d %s: %s (request_id %s)\n",
$status,
$error['code'] ?? 'error',
$error['message'] ?? $raw,
$reply['request_id'] ?? 'none'
));
exit(1);
}
$data = $reply['data'];
echo $data['status'], ' ', $data['tracking']['number'], ' ', $data['tracking']['carrier'], "\n";
echo $data['tracking']['page_url'], "\n";Sandbox orders run the real lifecycle on a compressed clock: created → in_production → shipped → delivered, firing genuinely HMAC-signed webhooks at every transition through the same delivery pipeline live orders use. Leave the tracking page open.
The sandbox order above never charges, prints or ships. A real one takes four changes.
A live order is charged to the card on file when it is placed. Without one, POST /v1/order answers 402 no_payment_method and nothing is placed.
Open BillingOn the API keys page, create a key in Live mode. It starts with ps_live_ and is shown in full once, so store it where your server reads secrets, never in a browser.
Create a live keySwap in the live key, your customer's address and your artwork: an https link to the file (PNG, PDF or SVG), or the id POST /v1/assets returns, as print_file. A live order's artwork is checked before anything is charged: a line with no artwork answers 422 artwork_required, and a file that cannot be printed answers 422 artwork_not_printable.
A subscription in test mode receives sandbox events only. For real orders, subscribe your endpoint in live mode (or all) on the Webhooks page or with POST /v1/webhooks, and verify X-PS-Signature-V2 on every delivery.
Open Webhooks