# How I Built a Free Package Tracking Notifier with Cloudflare Workers

I recently ordered **Starlink**. It shipped via **DHL**, and then got forwarded to a **local courier** that had **no push notifications**. I didn’t want to keep refreshing their tracking page, so I built a free notifier with **Cloudflare Workers**, **KV**, and **IFTTT**. This post explains the approach and gives a **step-by-step guide** you can follow.

---

## How It Works (Quick Recap)

1. A Cloudflare Worker fetches the courier’s tracking API (JSON).
    
2. It compares the latest response with the last one stored in **KV**.
    
3. If there’s a change, it posts a webhook to **IFTTT** (which sends me a push/email).
    
4. A scheduler (cron) runs the Worker every 30 minutes.
    

---

## Final Worker Script (with secrets + timeout + readable timeline)

```javascript
export default {
  async fetch(request, env, ctx) {
    // respond immediately to avoid cron timeouts
    ctx.waitUntil(handleTracking(env));
    return new Response(JSON.stringify({ status: "Queued" }), {
      headers: { "Content-Type": "application/json" },
    });
  },
};

async function handleTracking(env) {
  const API_URL = env.TRACKING_API_URL; // store in wrangler.toml/env
  const IFTTT_WEBHOOK_URL = env.IFTTT_WEBHOOK_URL; // stored as a secret

  try {
    // fetch with an explicit timeout
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 15000);

    const response = await fetch(API_URL, { signal: controller.signal });
    clearTimeout(timer);

    if (!response.ok) throw new Error(`API error: ${response.status}`);
    const current = await response.json();

    // read previous snapshot
    const previousRaw = await env.TRACKING_CACHE.get("previous_response");
    const previous = previousRaw ? JSON.parse(previousRaw) : null;

    // format timeline into readable text (limit to latest 5 entries to keep push short)
    const timeline = Array.isArray(current.timeline) ? current.timeline : [];
    const latest = timeline.slice(-5);
    const timelineText = latest
      .map(
        (item, i) =>
          `${i + 1}. [${item.date_time}] ${item.message} at ${item.location} (Status: ${item.status})`
      )
      .join("\n");

    // only notify if changed (or if nothing stored yet)
    const changed = !previous || JSON.stringify(previous) !== JSON.stringify(current);
    if (changed) {
      await fetch(IFTTT_WEBHOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ update: timelineText }),
      });
      console.log("Change detected → IFTTT notified");
    }

    // persist latest snapshot
    await env.TRACKING_CACHE.put("previous_response", JSON.stringify(current));
  } catch (err) {
    console.error("handleTracking error:", err);
  }
}
```

---

## Step-by-Step Setup Guide

### 1) Prerequisites

* A Cloudflare account (Workers + KV enabled on the free tier)
    
* Node.js + npm installed locally
    
* An **IFTTT** account with the **Webhooks** service enabled
    
* A courier tracking API URL (replace with yours)
    

---

### 2) Initialize the Worker Project

```bash
# install wrangler globally (or use npx wrangler)
npm i -g wrangler

# create a new worker
wrangler init tracking-notifier
cd tracking-notifier
```

---

### 3) Create a KV Namespace

> Wrangler v3 syntax uses spaces, not colons.

```bash
wrangler kv namespace create TRACKING_CACHE
```

Copy the returned `id`. If you use environments (dev/prod), also create a preview namespace:

```bash
wrangler kv namespace create TRACKING_CACHE --preview
```

Then add to `wrangler.toml`:

```toml
name = "tracking-notifier"
main = "src/index.js"
compatibility_date = "2024-01-01"

kv_namespaces = [
  { binding = "TRACKING_CACHE", id = "YOUR_NAMESPACE_ID" }
]

# Optional: built-in Cloudflare cron (every 30 minutes)
triggers = { crons = ["*/30 * * * *"] }

[vars]
TRACKING_API_URL = "https://<>.lk:3001/api/tracking/<number>"
```

> If you prefer environment sections:

```toml
[env.production]
kv_namespaces = [
  { binding = "TRACKING_CACHE", id = "PROD_NAMESPACE_ID" }
]

[env.preview]
kv_namespaces = [
  { binding = "TRACKING_CACHE", id = "PREVIEW_NAMESPACE_ID" }
]
```

---

### 4) Store Secrets (Don’t hardcode your IFTTT key)

Create the IFTTT webhook URL like:

```plaintext
https://maker.ifttt.com/trigger/ship/json/with/key/<REPLACE_WITH_YOUR_KEY>
```

Save it as a Worker secret:

```bash
wrangler secret put IFTTT_WEBHOOK_URL
# paste your full webhook URL when prompted
```

Now the code can read `env.IFTTT_WEBHOOK_URL`.

---

### 5) Wire Up IFTTT

1. In IFTTT, create a new **Applet**.
    
2. **If This** → choose **Webhooks**, event name: `ship`.
    
3. **Then That** → choose **Notifications** (or Email/Telegram/Slack).
    
4. In the message body, include `{{update}}` to display the timeline text.
    
5. Go to the Webhooks **Documentation** page in IFTTT to confirm your key and test.
    

**Manual test (optional):**

```bash
curl -X POST "$IFTTT_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"update":"IFTTT test from curl"}'
```

---

### 6) Add the Worker Code

* Place the JavaScript from **“Final Worker Script”** into `src/index.js`.
    
* Make sure the imports/structure match your `wrangler.toml` (the `main` field).
    

---

### 7) Test Locally & Deploy

**Local dev (uses a local preview KV binding):**

```bash
wrangler dev
```

Open the local URL shown in the terminal to trigger the fetch once.

**Deploy:**

```bash
wrangler deploy
```

**Tail logs (great for debugging):**

```bash
wrangler tail
```

---

### 8) Schedule It

**Option A — Cloudflare Cron Triggers (recommended)**

* We already added:
    
    ```toml
    triggers = { crons = ["*/30 * * * *"] }
    ```
    
* After deploy, Cloudflare runs your Worker every 30 minutes automatically.
    

**Option B — External Cron (**[**cron-job.org**](http://cron-job.org)**)**

* Create a cron job that hits your Worker URL every 30 minutes.
    
* Use method `GET` (or `POST`) and a timeout ≥ 10–15 seconds.
    
* Since our Worker responds immediately and does the work in the background (`ctx.waitUntil`), cron timeouts are unlikely.
    

---

### 9) Troubleshooting Tips

* **IFTTT not firing?** Ensure you send `{"update":"...text..."}` in the body and your event name matches (`/trigger/ship/...`).
    
* [**cron-job.org**](http://cron-job.org) **timeout?** We respond immediately and process in the background; verify you deployed that `ctx.waitUntil` version.
    
* **Large notifications truncated?** Limit timeline to the last few entries (as in the sample).
    
* **KV not updating?** Check your `kv_namespaces` binding name matches your code (`TRACKING_CACHE`).
    
* **API errors or slow courier API?** Increase the timeout slightly or add retry logic.
    

---

## Result

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1755411755285/f311e7e9-3c5d-49a8-874c-7fad0d1c80d6.jpeg align="center")

Now I get a neat push/email every time the local courier posts an update, even though they don’t offer native notifications. The stack is **free**, **reliable**, and easy to adapt for any other API you want to watch.

Happy automating! 🚀📦
