Edge runtime
Hono + TypeScript on Cloudflare Workers, served from Cloudflare’s global network. Cold starts are effectively zero.
Developer documentation
Every header, column and rule on this page is transcribed from the shipped implementation. Anything not built yet says so — plainly.
Hangerline runs entirely on edge infrastructure — there is no origin server to slow down or fall over.
Hono + TypeScript on Cloudflare Workers, served from Cloudflare’s global network. Cold starts are effectively zero.
Cloudflare D1 (SQLite) with per-merchant data isolation enforced in every query path. Migration-versioned schema.
Stripe Connect — each boutique is its own merchant of record. Boutiques onboard through Stripe\u2019s embedded components, or connect an existing Stripe account via standard Connect OAuth (the platform stores only the account id, never merchant API keys). Currently operating in Stripe test mode during the pilot.
Significant account actions (imports, exports, integration changes) are audit-logged and visible to the merchant.
Register up to 5 HTTPS endpoints per boutique from the dashboard. Each endpoint gets a whsec_ signing secret, and every delivery is signed so your server can prove the payload came from Hangerline and was not altered.
POST https://your-server.example/webhooks/hangerline
content-type: application/json
user-agent: Hangerline-Webhooks/1.0
x-hangerline-event: inventory.adjusted
x-hangerline-signature: 3f1a9c…(64 hex chars)…b2e
{
"event": "inventory.adjusted",
"created_at": "2026-08-03T18:04:11.000Z",
"data": {
"product_id": 42,
"sku": "DRS-SLK-08",
"quantity_before": 5,
"quantity_after": 4
}
}
x-hangerline-signature is the lowercase-hex HMAC-SHA256 of the raw request body, keyed with your endpoint's whsec_ secret. Verify against the raw bytes before JSON parsing, using a constant-time compare.
product.created · product.updated · product.deleted · import.completed · inventory.adjusted · giftcard.issued · giftcard.redeemed · loyalty.member.created · loyalty.points.changed · pos.sale.completed · pos.sale.refunded · purchase_order.created · purchase_order.received · appointment.booked · online_order.confirmed · online_order.completed
import { createHmac, timingSafeEqual } from 'node:crypto'
import express from 'express'
const app = express()
// IMPORTANT: verify against the RAW body, before any JSON parsing.
app.post('/webhooks/hangerline', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.BQOS_WEBHOOK_SECRET // whsec_... from the dashboard
const theirs = req.get('x-hangerline-signature') || ''
const ours = createHmac('sha256', secret).update(req.body).digest('hex')
const a = Buffer.from(ours), b = Buffer.from(theirs)
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).send('bad signature')
}
const event = JSON.parse(req.body)
console.log(req.get('x-hangerline-event'), event.data)
res.sendStatus(200)
})import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/hangerline")
def hangerline_webhook():
secret = os.environ["BQOS_WEBHOOK_SECRET"] # whsec_... from the dashboard
theirs = request.headers.get("x-hangerline-signature", "")
ours = hmac.new(secret.encode(), request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(ours, theirs):
abort(401)
payload = request.get_json()
print(request.headers.get("x-hangerline-event"), payload["data"])
return "", 200pos.sale.completed consumerA complete consumer that verifies the signature, ignores events it doesn't care about, deduplicates, and reacts to a finished register sale — the most common integration (sync a sale into your accounting system, trigger a thank-you email, update an external stock feed).
import { createHmac, timingSafeEqual } from 'node:crypto'
import express from 'express'
const app = express()
const seen = new Set() // in production, use your DB for idempotency
app.post('/webhooks/hangerline', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verify the signature against the RAW body (constant-time compare).
const secret = process.env.BQOS_WEBHOOK_SECRET
const theirs = req.get('x-hangerline-signature') || ''
const ours = createHmac('sha256', secret).update(req.body).digest('hex')
const a = Buffer.from(ours), b = Buffer.from(theirs)
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).send('bad signature')
// 2. Route by event type — ACK everything you don't handle so nothing queues up.
if (req.get('x-hangerline-event') !== 'pos.sale.completed') return res.sendStatus(200)
const { data } = JSON.parse(req.body)
// 3. Deduplicate: there are no automatic retries today, but building
// idempotently now means retries-with-backoff can ship without breaking you.
if (seen.has(data.sale_id)) return res.sendStatus(200)
seen.add(data.sale_id)
// 4. React to the sale — e.g. push it into your accounting system.
console.log('Sale #' + data.sale_id + ': ' + (data.total_cents / 100).toFixed(2) + ' USD')
console.log(' tax:', (data.tax_cents / 100).toFixed(2))
console.log(' tender:', data.tender) // { cash_cents, card_cents, gift_card_cents }
for (const item of data.items) {
console.log(' ' + item.quantity + 'x ' + item.name +
' (' + (item.sku ?? 'no SKU') + ') — ' + (item.line_total_cents / 100).toFixed(2))
}
if (data.loyalty_member_id) {
console.log(' loyalty member #' + data.loyalty_member_id + ' earned ' + data.points_earned + ' pts')
}
// 5. Respond 2xx within 10 seconds — do slow work (email, sync) async after ACKing.
res.sendStatus(200)
})
data for pos.sale.completedsale_id, total_cents, tax_cents, tender breakdown (cash_cents / card_cents / gift_card_cents), items (name, sku, quantity, line totals) and — when a loyalty member was attached — loyalty_member_id and points_earned. Every figure is the recorded register amount; nothing is recomputed on the way out.
Tokenized, always-current catalog feeds for Google Merchant Center (RSS 2.0 XML) and Meta Commerce Manager (CSV). Paste the URL once — the feed stays in sync with your catalog.
GET /feeds/ft_<40-hex-token>/google.xml
GET /feeds/ft_<40-hex-token>/meta.csv
The token authenticates your catalog only, is unguessable, and can be rotated from the dashboard at any time — old URLs stop working immediately.
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
<title>Heather's Boutique — Product Feed</title>
<item>
<g:id>DRS-SLK-08</g:id>
<g:title>Silk Wrap Dress — Emerald</g:title>
<g:link>https://your-store.example/products/silk-wrap-dress</g:link>
<g:image_link>https://your-store.example/img/silk-wrap.jpg</g:image_link>
<g:price>148.00 USD</g:price>
<g:availability>in stock</g:availability>
<g:condition>new</g:condition>
<g:brand>Velvet & Vine</g:brand>
</item>
</channel>
</rss>
Only products with a price, image URL and product page link are emitted — items missing any of these are excluded (Google and Meta would disapprove them anyway). The dashboard shows exactly which items are held back and why. Nothing is ever invented to make an item pass review.
id, title, description, availability, condition, price, link, image_link, brand, gtin
Availability derives from live quantity (in stock / out of stock). Feeds are cached for 15 minutes and capped at 5,000 items.
Bring your catalog from your current POS in minutes. The importer auto-detects the export formats of Clover, Square, Shopify and Lightspeed Retail and maps their columns automatically — or use the universal template.
name, sku, barcode, category, brand, price, cost, quantity, description, image_url, product_url
Only name is required. Prices are parsed as dollars; malformed URLs are dropped rather than stored dirty. Every import run reports created / updated / skipped counts truthfully, row by row.
Clover
Dashboard → Inventory → Items → Export
Square
Dashboard → Items & orders → Items → Actions → Export library
Shopify
Admin → Products → Export → CSV for all products
Lightspeed Retail
Inventory → Item Search → Export
Direct API sync with these systems is planned — it requires per-platform developer credentials and is not offered as “connected” until it truly is. CSV import is the honest, working path today.
One-click bank-import CSVs your bookkeeper can load directly. Exports cover your most recent 5,000 succeeded and refunded payments; refunds are represented correctly, not fudged.
Date, Description, Credit, Debit
Sales land in Credit; refunds land in Debit. Each row carries a truthful description with the payment reference.
Date, Amount, Payee, Description, Reference
Refunds are negative amounts. Reference is the Stripe payment intent ID for reconciliation.
Connect your own Klaviyo (contact sync), Shippo (live carrier rates + label purchase) and Twilio (SMS) accounts by pasting your own API keys. Three rules make these connections trustworthy:
The status turns green only after a live verification call to the provider succeeds. A bad key shows “Key error” with the provider’s real rejection reason — never a fake green light.
Keys are stored server-side and displayed only as ••••1234. They are never echoed back in full, in any response, ever.
Each provider bills you directly on your own account. Shippo rates are relayed verbatim from carriers — we add nothing.
Every connection action — verifications, syncs, sends, label purchases, failures included — is written to a visible activity log in your dashboard.
TLS everywhere; HSTS with includeSubDomains enforced on every response.
Hardened headers on all pages: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, strict Referrer-Policy, locked-down Permissions-Policy.
Passwords stored as salted hashes; sessions via an HttpOnly, SameSite=Lax cookie — unreadable by page scripts.
Card data never touches the platform: Stripe Terminal encrypts at tap/dip; verification and banking flow through Stripe’s PCI-scoped components.
Per-merchant isolation in every query path; merchant and platform-admin roles are separated surfaces.
No claimed certifications we don’t hold: no SOC 2 or ISO 27001 audit has been performed yet, and we say so — the measures above are what actually protects your data.
Things this page does not document as available, because they aren't yet:
Public token-based REST API
Today’s APIs are session-authenticated dashboard APIs. A keyed public API is planned; when it ships, its reference will live here.
Direct POS sync (Clover / Square / Shopify / Lightspeed)
Requires per-platform developer credentials. CSV import is the working path today, and the dashboard says so honestly.
Webhook automatic retries
Failed deliveries are logged visibly; retry-with-backoff is planned.
Live payments & hardware checkout
Stripe is in test mode during the pilot; live processing and one-click hardware ordering open after production approval.
See the integrations in action — including a live signature-verification demo that runs in your browser.
Explore the integrations showcase