UnifyCart Developers

Building an App, Step by Step

A worked example from an empty directory to an installed app, with the decisions explained at each step.

We will build a small app that adds a delivery estimate to the checkout, so a shopper sees "arrives Tuesday" rather than nothing.

It is deliberately small, and it exercises every part: installation, a token, reading shop data, a checkout extension, a webhook, and uninstalling cleanly.

Step 1: Decide What It Needs

Before any code, write down the permissions and be mean about it.

Our app needs to read products, because the estimate depends on what is in the basket. It does not need customers, orders or inventory.

The permission list is a sales document

A merchant sees it at install. An app asking for customer data to show a delivery date is an app that does not get installed by anybody paying attention.

Step 2: Stand Up Somewhere to Receive Calls

Your app is a web service. At minimum it needs three addresses:

POST /install      the shop tells you a merchant installed you
POST /render       the shop asks you for the checkout block
POST /webhook      the shop tells you something happened

All three must be HTTPS and answer quickly.

Step 3: Handle the Install

When a merchant installs your app you receive an access token for their shop. Store it against the shop, encrypted.

// Keep one row per installation. Never one token for all shops.
Installation::updateOrCreate(
    ['shop_domain' => $request->input('shop_domain')],
    ['access_token' => encrypt($request->input('access_token')), 'active' => true],
);

Then confirm which shop you are actually in, rather than trusting the request:

curl -s https://shop.example.com/api/apps/shop \
  -H "X-UnifyCart-Access-Token: THE_TOKEN"

Never trust a shop identifier that arrived in a request body

The token is the only thing that proves which shop a call concerns. An app that reads the shop from a parameter can be made to act on somebody else's.

Step 4: Register Your Extension

Still during install, tell the shop where you want to appear.

curl -s -X POST https://shop.example.com/api/apps/extensions \
  -H "X-UnifyCart-Access-Token: THE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "checkout_block",
    "callback_url": "https://your-app.example.com/render",
    "timeout_ms": 2000
  }'

We set a 2000ms timeout rather than the 5000 default, because we would rather vanish than delay somebody's checkout.

Step 5: Subscribe to What You Need

curl -s -X POST https://shop.example.com/api/apps/webhooks \
  -H "X-UnifyCart-Access-Token: THE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "topic": "order.placed" }'

We want order.placed so we can record what we estimated against what was actually ordered, and improve.

Subscribe to what you use. Every unused subscription is work the shop does for nothing.

Step 6: Render Something

The shop calls your callback_url during checkout and expects a quick answer.

public function render(Request $request)
{
    // Verify the call really came from the shop before doing anything.
    abort_unless($this->signatureValid($request), 401);

    $shop = $this->shopFor($request);
    $estimate = $this->cachedEstimateFor($shop, $request->input('items', []));

    return response()->json([
        'html' => view('block', ['estimate' => $estimate])->render(),
    ]);
}

Note what this does not do: it does not call the shop's API, and it does not calculate anything slow. Both would put your latency in front of a shopper. The estimate was worked out earlier and cached.

Step 7: Do the Slow Work Elsewhere

Our estimate depends on product data. Fetch it on a schedule, not during a render.

curl -s "https://shop.example.com/api/v1/products?per_page=100" \
  -H "Authorization: Bearer THE_TOKEN"

Page through once, keep what you need, and refresh on a sensible interval. Then use order.placed to learn about changes rather than re-reading the catalogue hourly.

Step 8: Handle the Webhook Properly

public function webhook(Request $request)
{
    abort_unless($this->signatureValid($request), 401);

    // Answer first, work afterwards. A slow handler is a failed delivery.
    dispatch(new RecordOrder($request->all()));

    return response()->noContent();
}

Then, in the job, treat it as something that may run twice:

// Deliveries are retried, so the same event can arrive after you handled it.
if (ProcessedEvent::where('event_id', $payload['id'])->exists()) {
    return;
}

Step 9: Test It Against a Real Shop

Install it on a shop with a real catalogue. Then, deliberately:

  • Put forty items in a basket and check your render still answers in time
  • Make your service return a 500 and confirm checkout still completes without your block
  • Make your service time out and confirm the same
  • Send yourself the same webhook twice and confirm nothing is recorded twice

The second and third are the ones that get skipped and the ones that take a merchant's shop down.

Step 10: Uninstall Cleanly

When the merchant removes your app, remove what you added.

curl -s -X DELETE https://shop.example.com/api/apps/extensions/{id} \
  -H "X-UnifyCart-Access-Token: THE_TOKEN"

Then stop calling the shop, mark the installation inactive, and cancel any scheduled work for it.

Test the uninstall as carefully as the install

Install, use the shop, uninstall, and then look at the shop. It should be exactly as it was. An extension or script tag left behind by an app that is gone is the usual cause of a layout fault after an uninstall, and the merchant will blame the platform rather than you.

Step 11: Decide What Happens to Data

Say in your listing what you keep after an uninstall and for how long.

A merchant who uninstalls and finds your data still attached to their products, or who reinstalls and finds their settings gone, will say so publicly. Either answer is acceptable. Not having one is not.

What This Example Left Out

Billing, if you charge. Settings, if the merchant configures anything. Multiple extension points. And translations, which matter the moment a shop is not in English.

The shape stays the same: install, store one token per shop, register what you need, answer fast, verify everything, and remove it all cleanly.