> For the complete documentation index, see [llms.txt](https://docs.adpage.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.adpage.io/data-and-insights/marketing-integrations/craft-commerce.md).

# Craft Commerce

Connect Craft Commerce for weekly revenue and the postcode data the geo model runs on, through a small read-only feed on your own shop.

Connect Craft Commerce so AdPage reads order revenue and the region an order was shipped to. Unlike the other webshop integrations, there are no credentials to create in an admin screen: your shop serves a small read-only endpoint that AdPage then reads.

AdPage only reads.

{% hint style="info" %}
Works with **Craft Commerce 3, 4 and 5**. Verified against Craft 5.11 with Commerce 5.7.
{% endhint %}

### Why this one works differently

Shopify, Shopware, Magento and Lightspeed all ship an API for reading orders. Craft Commerce does not — orders are not part of its GraphQL schema, and there is no REST equivalent. There is nothing to point a connector at.

So instead of a connector, there is a contract: one endpoint, defined by AdPage, that your shop serves. You install a file we supply; no custom code has to be written.

This means there is **work on your side before any data flows**. It is small — 15 to 30 minutes for whoever maintains the site, including a deploy — but it needs a developer, not an admin login.

### What AdPage asks for, and what it does not

| Asked for                                  | Never asked for             |
| ------------------------------------------ | --------------------------- |
| Order date and total                       | Order lines or products     |
| Country, postcode and city of the shipment | Customer names or addresses |
| The domain of the order e-mail             | Full e-mail addresses       |
| Read access through one key                | Write access of any kind    |

The e-mail domain is used for one thing only: recognising marketplace orders, which arrive under a bol.com or Amazon proxy address. The domain is enough for that, so the address itself never leaves your shop.

The postcode does travel, because that **is** the geographic signal the model uses. Treat the key as a real secret and serve the endpoint over HTTPS.

### Two ways to install

**Element API (recommended).** A free first-party Craft plugin plus one configuration file. No PHP of your own.

**A small module.** Two files in your `modules/` folder. For shops that would rather not add a plugin — and the easier route if `allowAdminChanges` is `false` on production, because it does not touch project config.

Both serve exactly the same thing. AdPage cannot tell the difference.

### Signed requests

Besides the key, every call AdPage makes carries two extra headers:

```
X-AdPage-Timestamp: 1790000000
X-AdPage-Signature: <hex>
```

The signature is an HMAC-SHA256 over four lines — method, path, sorted query string and the timestamp — with the feed key as the secret. Your shop recomputes it and compares. The timestamp window is five minutes either way, so a call captured off the wire cannot be replayed the next day.

The files we supply already do this. They **verify a signature whenever one is present**, but only **require** one once you set:

```
ADPAGE_FEED_REQUIRE_SIGNATURE=true
```

That order is deliberate: install, confirm in your logs that signed calls are arriving and being accepted, and only then switch it on. There is no moment at which both sides have to change together.

### Connect Craft in AdPage

{% stepper %}
{% step %}

#### Open the tile

Go to **Integrations**, select the container, and open the **Craft Commerce** tile. Choose an installation method.

<figure><img src="https://1954565367-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FK5kjEu8mwstNkOrvFl6S%2Fuploads%2FZxMacjTZG29rjy1wRznf%2Fintegrations-craft-commerce-tile-en.webp?alt=media" alt="Craft Commerce tile opened with the feed URL field"><figcaption><p>The Craft Commerce tile on the Integrations page.</p></figcaption></figure>
{% endstep %}

{% step %}

#### Generate the key

AdPage generates the key for you and shows the `.env` line to paste. You do not invent one, and nothing has to be typed over.
{% endstep %}

{% step %}

#### Hand it to your developer

If you are not doing this yourself, generate a **share link**. It opens a page with the instructions, the key and the files on it — everything needed, nothing else.

The link is valid for 14 days, can be revoked at any moment, and generating a new one revokes the old. AdPage shows how often it has been opened.
{% endstep %}

{% step %}

#### Check the installation

Fill in your shop URL — the root, `https://yourshop.nl`, without the endpoint path — and select **Check installation**.

AdPage calls the endpoint and reports what it sees: whether it answers, whether the key is accepted, how many orders are in the window, what share of them carry a postcode, the currency, and how much history is available.
{% endstep %}

{% step %}

#### Pull in the history

Run the backfill to fetch two years of orders in four-week blocks. It runs in the background; you can close the page.

Two years is roughly 26 blocks, spread out over several minutes rather than fired off in one run — see *How often AdPage calls your shop* below.
{% endstep %}
{% endstepper %}

### What you get

Weekly order revenue as the figure the **Marketing Mix Model** explains, and week × province revenue for the **geo model** that measures regional advertising lift.

Where your shop sells through several stores or sites — a B2B channel alongside the consumer shop, say — those can be separated, which matters because B2B orders are not driven by the advertising in your reports.

### How often AdPage calls your shop

There is never more than one request open at a time, and the pace is deliberately slow.

|                    |                           |
| ------------------ | ------------------------- |
| Between two pages  | 300 ms                    |
| Between two blocks | 2 s                       |
| Blocks per run     | 6, then a minute of quiet |
| Request timeout    | 30 s                      |

The two-year backfill therefore spreads over roughly five minutes with gaps in it, instead of arriving as one unbroken run.

Once the history is in, the only recurring call is **Monday at 02:00 UTC** — 03:00 or 04:00 in Amsterdam, depending on the season. That time was chosen to sit well clear of the morning and end-of-day traffic peaks most shops see.

The weekly run re-reads the **last three weeks**, not just the last one. An order can be completed or corrected after the fact, and that changes a week total retroactively; reading only the past week would miss those corrections permanently. Three weeks is a single four-week block, so a handful of requests, and the write is idempotent.

**When your server struggles**

| Situation                     | What AdPage does                                                                             |
| ----------------------------- | -------------------------------------------------------------------------------------------- |
| Timeout, 429 or 5xx           | Retries, three attempts at most, waiting 2 and then 8 seconds                                |
| `Retry-After` in the response | Follows your wait instead of ours, capped at 30 s                                            |
| 4xx other than 429            | No retry. A wrong key or a missing endpoint will not answer differently on the third attempt |
| Three blocks failing in a row | The run is abandoned. The job stays where it is and the next run is a minute later           |

That last row is the one that matters most: a server that has just fallen over does not get the rest of the window thrown at it back to back.

### Reading the numbers

**Totals include VAT.** `totalPrice` in Craft is the order total including VAT and shipping, matching how Magento, Shopware and Lightspeed report. The model explains the revenue your shop takes, not product margin.

**The postcode is the only regional signal there is.** Craft builds addresses on an international address library, and the address format for the Netherlands has no province field — nor does Germany's. So the region field comes back empty on every Dutch order, always. That is correct behaviour, not a misconfiguration.

AdPage derives the province from the postcode instead. The consequence matters: **if shipping postcodes are missing, there is no fallback and no geo model.** That is why postcode coverage is the one check that has to pass.

**Carts are excluded.** Only completed orders count. An abandoned basket counted as revenue would show the model a demand spike that never happened.

**A fresh Commerce store is set to USD.** If your currency check fails, that default was probably never changed. AdPage does not silently convert.

**The shop time zone decides the week.** An order placed just after midnight local time falls in the previous day in UTC, which can move it into the previous ISO week. Set the time zone to match where you trade.

### Troubleshooting

**404 on the endpoint.** With Element API, the file has to be named exactly `config/element-api.php` — no other name is read, and a wrong one fails silently. With the module, check that it is registered under `bootstrap` in `config/app.php` and that `composer dump-autoload` has run.

**403.** The key does not match, or `ADPAGE_FEED_KEY` is empty on the server. Most deploys do not reload `.env` without a restart, and this is by far the most common cause.

**403 immediately after switching on `ADPAGE_FEED_REQUIRE_SIGNATURE`.** Check the server clock before anything else. The timestamp window is five minutes either way, so a host whose clock has drifted further than that rejects every call while the key is perfectly fine. Setting the flag back to `false` confirms whether that is what you are looking at.

**500 across the whole site, not just the endpoint.** Craft loads the Element API config while loading plugins, before routing — so a fatal error in that file takes the shop down, not just the feed. Run `php -l config/element-api.php` and load one ordinary page before deploying to production.

**`plugin/install` fails on production.** `allowAdminChanges` is `false`, which is the recommended production setting. Install on a development environment, commit your project config, and let production pick it up through `composer install`. Or use the module, which avoids project config entirely.

**Empty data while orders exist.** Almost always the date window: the end date is exclusive. Check too that the orders are completed.

**Every region is empty.** Expected — see *Reading the numbers* above. Check postcode coverage instead.

**All amounts are €0.00.** The shop has no valid prices, or on Commerce 5 the variant's `basePrice` was never filled in. Commerce 5 moved variant pricing: `basePrice` is the field you set, while `price` became derived and read-only — and writing to the read-only one saves without complaint and without effect.

Still stuck? Contact <support@adpage.io> with the container and the shop URL — never the key.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.adpage.io/data-and-insights/marketing-integrations/craft-commerce.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
