> ## Documentation Index
> Fetch the complete documentation index at: https://print.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate fulfilment from WooCommerce

> When a WooCommerce order is paid, fetch each customer's print-ready PDF and send it to your print partner.

With the [Print.App WordPress plugin](https://print.app/docs/platforms/wordpress/installation.md), every WooCommerce order line that has a design carries its project. This guide reads the project from a paid order, downloads its print-ready PDF and hands it to a print partner or fulfilment API.

> [!NOTE]
>
> The plugin can also send paid orders to a [fulfilment provider](#fulfilment-providers) for you, with no code. To fetch files by hand instead, they are listed against each order in your WordPress admin: see [How to access files](https://print.app/docs/platforms/wordpress/how-to-access-files.md).

## 1. Find the project on the order

The plugin stores the design on each order line as the item meta `print_app_customization`, an array that holds the `projectId`:

```php theme={null}
foreach ($order->get_items() as $item) {
    $customization = $item->get_meta('print_app_customization', true);
    $project_id = is_array($customization) ? ($customization['projectId'] ?? '') : '';
}
```

Lines without a design have no such meta.

## 2. Download the print-ready PDF

The PDF is at:

```
https://pdf.print.app/{projectId}
```

For other formats, swap `pdf` for `png`, `jpeg` or `tiff` in the host name.

* **It renders on request.** The file is produced from the saved project when you ask for it, which takes a few seconds. Allow a generous timeout.
* **It answers with a redirect** to a short-lived download link. Follow the redirect. `wp_remote_get` does so by default.
* **Download it once, when the order is paid, and keep your own copy.** A later download reflects the project's state at that later time.
* **The link works without a key.** Treat it as confidential, and don't publish it.

The PDF's page size and settings come from the design. Set the size in the design's pages, and choose the PDF version, colour profile and fonts with a [PDF output profile](https://print.app/docs/guides/projects/pdf-output-profiles.md).

> [!TIP]
>
> To check from your server that the project is yours and finished before printing, call [`GET /v2/projects/{id}`](https://print.app/docs/api-reference/v2/projects/get) with a [secret key](https://print.app/docs/api-reference/v2/authentication.md). Its `exports.pdf` is the same link. See [Verify a project before fulfilling](https://print.app/docs/api-reference/v2/verify-a-project.md).

## 3. Send the order to your print partner

This runs when an order becomes *processing*, which for most payment methods means it has been paid. It collects each cover PDF, stores a copy and creates one print job per order. Add it to a small plugin or your theme's `functions.php`:

```php theme={null}
add_action('woocommerce_order_status_processing', function ($order_id) {
    $order = wc_get_order($order_id);
    if (!$order || $order->get_meta('_print_job_id')) return;   // already sent

    $files = [];
    foreach ($order->get_items() as $item) {
        $customization = $item->get_meta('print_app_customization', true);
        $project_id = is_array($customization) ? ($customization['projectId'] ?? '') : '';
        if (!$project_id) continue;

        // Rendered on request, then redirected to a short-lived download link
        $response = wp_remote_get('https://pdf.print.app/' . rawurlencode($project_id), ['timeout' => 120]);
        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
            error_log("Print.App PDF for order $order_id failed");
            return;   // stop; retry this order later rather than send it partly
        }

        // Your own copy, at an unguessable URL your print partner can fetch
        $name = sprintf('print-%d-%s.pdf', $order_id, wp_generate_password(24, false));
        $upload = wp_upload_bits($name, null, wp_remote_retrieve_body($response));
        if (!empty($upload['error'])) {
            error_log("Saving the PDF for order $order_id failed: {$upload['error']}");
            return;
        }
        $files[] = ['item' => $item, 'url' => $upload['url'], 'project_id' => $project_id];
    }
    if (!$files) return;

    // Your print partner's API: one line item per file
    $job_id = my_create_print_job($order, $files);
    if ($job_id) {
        $order->update_meta_data('_print_job_id', $job_id);
        $order->save();
    }
});
```

`my_create_print_job` is your own call to your partner's API. A few things to decide for production:

* **Retries.** If a download or the partner's API fails, the hook stops without marking the order. Re-run it for that order later, for example with a scheduled action.
* **Storage.** Media uploads are public URLs. The random file name keeps them unguessable, but you can also store the files privately and delete them once your partner has fetched them.
* **Idempotency.** The `_print_job_id` check stops the same order being sent twice when its status changes again.

## Fulfilment providers

The plugin can send paid orders to a fulfilment provider itself, so the code above isn't needed for that provider. Providers are optional and off until you set one up. In your WordPress admin, open **PrintApp** in the left menu and expand **Fulfilment providers** at the bottom of the page.

The providers available now:

* [Lulu](#lulu), for print-on-demand books, notebooks and journals

### Lulu

Sends orders to the [Lulu Print API](https://www.lulu.com/sell/sell-on-your-site/print-api) (plugin 2.4.0 and later). It's built for personalised notebooks and books: the customer designs the cover in Print.App, and the interior PDF is the same for every order.

**1. Get Lulu API keys**

Generate an API key and secret at [developers.lulu.com](https://developers.lulu.com/user-profile/api-keys). To test first without printing or paying for anything, use Lulu's [sandbox](https://developers.sandbox.lulu.com/user-profile/api-keys), which has its own keys.

**2. Switch it on**

In your WordPress admin, open **PrintApp**, expand **Fulfilment providers**, then **Lulu**. Switch on **Send paid orders to Lulu**, choose the environment (**Sandbox** or **Production**), enter the API key and secret, and pick a shipping level. Lulu writes to the contact email if it has questions about a print job; it defaults to the site admin email.

**3. Set up the product**

On the WooCommerce product, open the **Lulu** tab under **Product data** and fill in:

* **Lulu product (POD package ID)**, the book specification from Lulu's pricing calculator, such as `0600X0900.BW.STD.CO.060UW444.MXX`
* **Interior PDF URL**, a link Lulu can download your fixed interior PDF from
* **Interior page count**

Save the product. The tab then shows the **Cover size** for that book and page count, such as `311.2 × 235 mm`.

**4. Design the cover at that size**

Lulu prints the cover from **one PDF page** that holds the back cover, spine and front cover. Make the Print.App design a single page at exactly the size the Lulu tab shows. That size already includes bleed, so don't add any on top.

Lock the back cover and spine with the [object lock settings](https://print.app/docs/guides/designs/objects/settings.md), so customers personalise only the front. Assign a [PDF output profile](https://print.app/docs/guides/projects/pdf-output-profiles.md) that matches Lulu's file requirements.

**5. Test in the sandbox**

With the **Sandbox** environment, place an order for the product and mark it **Processing**. Within a few minutes the order notes show the Lulu print job. Use the order action **Refresh Lulu print job status** to see whether Lulu accepted the files. Switch to **Production** once it does.

> [!WARNING]
>
> Lulu downloads the cover PDFs from your site's uploads folder, so your site must be reachable from the internet. A shop running only on your own computer won't work.

#### What happens to an order

* **When an order becomes Processing**, the plugin sends it to Lulu in the background as one print job. Each order line with a Print.App design, on a product with a Lulu product and interior PDF set, becomes a line item with its quantity. Other lines are left out, so mixed orders are fine.
* **The cover** is downloaded from `pdf.print.app` and stored in your uploads folder under an unguessable name, for Lulu to fetch.
* **The address** is the order's shipping address, or the billing address when there is none. Lulu allows at most 30 characters in each street and city line, and 35 in the name.
* **Temporary failures** are retried after 15 minutes and again after an hour. These include a network error, a cover PDF that couldn't be generated, or Lulu being busy.
* **An order is never sent twice**, even if its status changes again.
* **Every result is written to the order notes:** the print job number, its status, and Lulu's reasons if it refuses an address or rejects a file.

Order actions, in the order's **Order actions** box:

| Action | When it shows | What it does |
| - | - | - |
| **Send Print.App designs to Lulu** | The order hasn't been sent | Sends it now, for example after you fix a product's Lulu settings |
| **Send Print.App designs to Lulu again** | Lulu rejected or canceled the order's print job (plugin 2.4.1 and later) | Sends the order as a new print job. The old job stays in your Lulu account, marked rejected or canceled |
| **Refresh Lulu print job status** | The order has a print job | Reads the job's latest status from Lulu, with tracking links once it ships |

#### Paying for print jobs

Lulu bills **your** Lulu account, not your customer. A new print job stays **Unpaid** until you pay for it in your Lulu account, unless you keep a payment card on file with Lulu. An order note such as *"print job #354632 is UNPAID. Lulu accepted the files"* means the files passed Lulu's checks and the job is waiting for payment.

#### If Lulu rejects a file

The order note gives Lulu's reason. The common ones are about the cover:

* **The PDF dimensions need to be within a range.** The design isn't the cover size shown in the product's Lulu tab.
* **The cover must be a single page.** The design has more than one page.

Fix the cause, then use the order action **Send Print.App designs to Lulu again**. A customer's project keeps the size of the design it was made from, so correct the design too, before more orders come in. Testing each product in the sandbox first catches both problems.

## Why not the "Project created" webhook?

The [Project created webhook](https://print.app/docs/configuration/webhooks.md) fires the first time a project is saved. That can be an autosave, before the customer has finished and before any order exists. It's useful for notifications, but for fulfilment start from the paid order as above.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.