A read-only packing worklist for one Etsy shop. Orders that still need to go out sit at the top, and everything outstanding is totalled into one pick list so you know what to pull off the shelf before you start.
Đơn hàng is Vietnamese for "orders".
donhang is a personal, non-commercial tool used by one person on one computer. It holds read-only access and writes nothing to Etsy. The term "Etsy" is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
- Lists the connected shop's 50 most recent orders, unshipped first — the point of a queue.
- Each order shows its items and quantities, the total, the delivery address, and the buyer's message, all on screen at once so you can pack straight from it.
- Totals every unshipped line into one pick list — the thing you actually want before packing, and the thing an order page can never show you.
- Shows the tracking already recorded on orders that have gone out.
- Falls back to sample data when nothing is connected, so the whole interface works before any key exists.
It has no write path — no marking shipped, no editing, no messaging. It asks Etsy for read
scopes only, and every call it makes to /v3/application/… is a GET. (The one POST in the
source is the OAuth token exchange at /v3/public/oauth/token, which the protocol requires and
which carries no shop data.) Shipping is done on Etsy; this page reflects it on the next refresh.
| Queue | Shipped |
|---|---|
![]() |
![]() |
node main.js # http://localhost:3007
npm run dev # same, with --watchThere is nothing to install. No dependencies, no bundler, no build step — Node's standard library only, and not a single line of JavaScript is served to the browser.
With no .env present it starts in demo mode.
- Register an application at https://www.etsy.com/developers/register. Describe it truthfully: a private order queue used only by the shop owner.
- On the Etsy app settings, add this Callback URL exactly:
http://localhost:3007/auth/callback cp .env.example .envand paste the keystring intoETSY_KEYSTRING.- Restart, open the page, press Connect my Etsy shop, approve the scopes.
| Scope | Why |
|---|---|
shops_r |
resolve which shop authorised the app |
transactions_r |
read that shop's receipts — the orders themselves |
Two read scopes, no write scope. An Etsy account with no shop can connect, it will simply have no receipts to show.
main.js http server, routes, .env reader — node:http, no framework
etsy.js Etsy v3 client: OAuth PKCE, token refresh, two GETs
orders.js the single normalizer both live and demo data pass through, plus the pick list
mock.js demo receipts in RAW Etsy shape (see below)
views.js server-rendered HTML, no client JS
store.js data/tokens.json — tokens + cached shop id, git-ignored
public/ style.css, favicon.svg, and the four static pages
Demo data is emitted in the raw Etsy receipt shape and normalized by the same
normalizeReceipt() that live responses go through. A field that breaks in production breaks in
the demo too. (The alternative — a mock that emits already-normalized objects — lets the two
drift apart silently, which is a real bug I did not want to repeat.)
x-api-keyis normally the bare keystring on OAuth calls, but some requests wantkeystring:shared_secretand say so in neither the docs nor the error body. SetETSY_SHARED_SECRETin.envand the client sends the pair.- The v3 access token carries the user id in front of the first
.— there is no "me" endpoint. - Money arrives as an integer plus a divisor;
orders.jsdivides once and keeps the currency code alongside. - Access tokens last an hour and are refreshed a minute early.
Orders are fetched per page view and held in memory only long enough to render. Nothing about an
order — buyer name, address, message — is written to disk, logged, or sent anywhere but back to
Etsy. The only file the app writes is data/tokens.json.
| Page | URL |
|---|---|
| Queue | / · /?show=shipped · /?show=all |
| What this is | /about |
| Privacy | /privacy |
| Terms of use | /terms |
| Contact | /support |
MIT.

