---
title: Testing
description: Test against a real shop running in test mode. Real cart, real checkout, real card rail, no money.
---

# Testing

**Testing is free. You place orders against a real Shopify shop that is running in test mode, using a test card, and no money moves.**

Everything about the run is real except the settlement: a live cart, real shipping rates for your destination, the shop's actual hosted checkout, the real card rail with the vault swapping your alias mid-flight. The gateway is the only part that is simulated.

---

## Why a development store would not work

Worth knowing, because it is the first thing everybody tries.

A Shopify **development** store can only take test payments, which sounds ideal. Two independent things make it useless.

Its Test payment gateway only accepts the literal card number `1`. Agnic never types a card number — it types a vaulted alias shaped like a card, and the vault swaps in the real number mid-flight. You cannot vault `1`, because it is not a card and will not survive the browser's own validation. So the one gateway that costs nothing is the one gateway this rail cannot use.

And a development store cannot drop its storefront password. Every path stops at the password page, including the cart link, and the checkout engine correctly classifies that as unreachable. Lifting the password means putting the store on a paid plan, at which point it is no longer a development store.

The way out is not a special store. It is an ordinary store with **Shopify Payments switched to test mode**, which accepts the normal test card numbers through the normal card fields.

---

## The sandbox

**`untitled-fidget.shop`** — a real Shopify shop, already onboarded, running in Shopify Payments test mode.

Its merchant id is **`merchant_untitled_fidget_shop`**, on the Shopify rail, and it has two items priced at **1.00 CAD**:

| Item | `sku` |
|---|---|
| Hex Token Fidget | `gid://shopify/ProductVariant/43945235349570` |
| Paw Print Charm | `gid://shopify/ProductVariant/43945255567426` |

It ships to **Canada and the US**, and prices in the destination's currency. It does **not** ship to the UK: a UK `ship_to` answers `unfulfillable` with no delivery options. That is the shop's shipping settings, not your code — it is exactly the answer your product will get from any real shop that cannot reach an address, so handle it here.

### Finding a SKU yourself

The sandbox shop is not in the `products/search` pool, and neither is any shop that has not been onboarded. Two ways to get SKUs for any Shopify merchant:

**Quote with a product name.** Put the name where the SKU goes. The quote fails with `422` and `code: "unknown_sku"`, and its `suggestions` list the real SKUs, ranked, with prices.

```bash
curl -s -X POST https://api.agnic.ai/api/autofill/shopify/quote \
  -H "X-Agnic-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{ "merchant_id": "merchant_untitled_fidget_shop", "items": [{ "sku": "hex token fidget", "quantity": 1 }] }' \
  | jq '.suggestions'
```

**Explore the shop.** `POST /api/autofill/explore` with the shop URL returns `status: "exploring"` and an `order_id`. Poll `GET /api/autofill/orders/{order_id}` for a few seconds until `status` is `explored`; the shop's SKUs are in `discovered_items.catalog`. The explore response itself never carries them.

`GET /api/autofill/merchants/{id}` returns `catalog: null` for a Shopify-rail merchant, and that is correct: its products come from Shopify at quote time, not from that field.

Confirm test mode for yourself from the checkout: the installments block reads *"Installments can't be used in test mode."* That is Shopify saying it, not us.

Buy the one-dollar items. Nothing stops you buying the expensive ones, but a fifty-dollar simulated order teaches you nothing a one-dollar one does not.

---

## Cards

Use a **test card**. Vault it once at `https://app.agnic.ai/partner/cards/new` — the number goes straight to the vault, and neither your servers nor Agnic's application code ever see it.

| | |
|---|---|
| Visa | `4242 4242 4242 4242` |
| Mastercard | `5555 5555 5555 4444` |
| Expiry | any future date |
| Security code | any three digits |
| Name on card | at least two words |

These are Luhn-valid sixteen-digit numbers, so they vault and reveal exactly like a real card. Nothing about the rail is bypassed.

Do **not** vault a real card for this. There is no reason to, and test mode would decline it anyway.

---

## Three things that will interrupt you

None of these are failures. All three cost people an afternoon before they were written down.

### The security code expires roughly hourly

The vault holds a card's security code for about fifty minutes and never longer — it is prohibited from keeping it. After that, your next dispatch returns `202` with `cvv_refresh_required`.

Open the `approval_url`, re-enter any three digits, then dispatch **the same saved request** again with the `approval_token`. Nothing was charged and nothing is broken.

### Your mandate must be in the store's currency

The policy engine will not convert between currencies, deliberately: converting at an unseen rate hides spending behind a stale number.

A mandate denominated in GBP against this shop's CAD order returns `202` with `currency_mismatch` on **every** dispatch, forever, until you issue one in CAD. The mandate form has a currency selector and it defaults to your profile's country, which is how most people end up with the wrong one.

### Do not poll by re-dispatching

A `cvv_refresh_required` step-up mints a **brand new token on every dispatch**. It does not report the pending one as not-ready.

So a retry loop that waits by calling dispatch again will never terminate. It spins, and it fills the approvals table with dead tokens. Poll `GET /api/approvals/{token}` until its `status` is `approved`, then dispatch once.

The mandate step-up behaves differently — it *does* answer `approval_not_ready`. Write your loop for the harsher of the two.

---

## What these orders are, and are not

No money moves. No parcel is shipped. The shop's own order records mark them as test orders.

They still carry **`test: false`** in the Agnic API. That field is about the *merchant* being a designated test merchant, which this one is not — it is an ordinary shop that happens to have its gateway in test mode. So do not branch on `test` to decide whether an order was practice. It will be false here.

There is no `TEST STORE` prefix on the preview summary here, for the same reason.

Because settlement is simulated, this is the one thing the sandbox does not prove: that a real card would have been accepted by a real gateway. Everything before that point is identical.

---

## Shipping somewhere other than the cardholder

Pass `ship_to` and the order goes to that address instead of the cardholder's, with the billing block filled from the card's own address.

**Your own billing address must be on your account first.** The billing block on the checkout is filled from your profile, not from the request — there is no field for it in the dispatch body. If the profile is missing a name, street, city, postal code or country, that block is left blank, the shop refuses the form, and you get a `409 billing_profile_incomplete` naming the fields. Set them at [app.agnic.ai/profile](https://app.agnic.ai/profile) or with `PATCH /api/profile`. It is the cardholder's address, not the recipient's.

Two things to know. The engine proves the destination is on the hosted checkout **before** it touches the card, and refuses outright if it cannot — a parcel to the wrong address is not something a retry fixes. And this path was materially broken on the Shopify rail until 13 September 2026, so if you tried it before then and gave up, try again.

---

## Deliberately breaking things

The failure paths are where an agent product is actually judged, and most of them happen before the card is ever touched.

Set `max_shipping_minor` to `1` and watch the constraint blocker arrive at preview, with no token issued and no browser launched.

Change one character of a `ship_to` postcode between preview and place, and watch `TOKEN_BODY_MISMATCH` come back before anything is dispatched.

Order a variant that is out of stock and read what `retryable: false` looks like next to `re_preview`.

Ask for `ship_to` at a merchant that is not on the Shopify rail and read the refusal.

Declines are simulatable too, now that the gateway is in test mode — Shopify publishes the card numbers that force a decline or a gateway error. Use those rather than inventing a failure your code will never actually see.

Doing each of these once, on purpose, is worth more than reading about them.
