# Why Vercel Blob onUploadCompleted never fires on localhost

> onUploadCompleted is a webhook from Vercel to your app. It cannot reach localhost, so the SDK skips it. Use a tunnel and VERCEL_BLOB_CALLBACK_URL, and never make the upload depend on it.

You add `onUploadCompleted` to `handleUpload()`. The upload succeeds. The
callback never runs. Your terminal shows:

```
onUploadCompleted provided but no callbackUrl could be determined.
```

Nothing is broken. It is working as designed.

## Why

`onUploadCompleted` is not called by the browser. It is called by **Vercel
Blob's servers**, over the internet, after the bytes land. The SDK builds the
callback URL when it issues the client token:

- `VERCEL_BLOB_CALLBACK_URL` if set.
- Otherwise, only when running on Vercel (`VERCEL=1`):
  `VERCEL_BRANCH_URL` or `VERCEL_URL` on preview,
  `VERCEL_PROJECT_PRODUCTION_URL` on production.

On your laptop none of those exist, so there is no URL, and the SDK skips the
callback. Even if it had one, Vercel cannot reach `localhost:3000`.

## Test it locally with a tunnel

```bash
ngrok http 3000
```

```bash
# .env.local
VERCEL_BLOB_CALLBACK_URL=https://abc123.ngrok-free.app
```

Restart the dev server. The SDK appends your route's path, so the callback
goes to `https://abc123.ngrok-free.app/api/upload`. Cloudflare Tunnel works
the same way.

Never set `VERCEL_BLOB_CALLBACK_URL` in your Vercel environment. On Vercel the
SDK already knows the right URL, and a stale tunnel URL there silently drops
every callback.

## Design so it does not matter

The callback is a second path, not the only one:

- **The browser gets the result directly.** `upload()` resolves with the
  blob's `pathname` and `url`. Save the key from there in a server action.
  That works everywhere, localhost included.
- **The callback is the backstop.** If the tab closes after the upload but
  before the save, only the callback knows the file exists. Use it to mark the
  row stored, or to start processing.

Both may run. Make the callback idempotent: an upsert on the key, not an
insert. A non-2xx answer makes Vercel retry it.

## Verifying the callback

`handleUpload()` checks the `x-vercel-signature` header against your
`BLOB_READ_WRITE_TOKEN` before it calls `onUploadCompleted`. Two consequences:

- Do not put your normal session or origin check in front of it. The caller is
  Vercel. It has no cookie and sends no `Origin`.
- Trust `tokenPayload` (your server signed it into the token), not anything
  the browser sent after the fact.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
