Developer documentation

The technical reference for Hangerline

Every header, column and rule on this page is transcribed from the shipped implementation. Anything not built yet says so — plainly.

Architecture Live

Hangerline runs entirely on edge infrastructure — there is no origin server to slow down or fall over.

Edge runtime

Hono + TypeScript on Cloudflare Workers, served from Cloudflare’s global network. Cold starts are effectively zero.

Edge database

Cloudflare D1 (SQLite) with per-merchant data isolation enforced in every query path. Migration-versioned schema.

Payments layer

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.

Accountability

Significant account actions (imports, exports, integration changes) are audit-logged and visible to the merchant.

Outbound webhooks Live

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.

Delivery format

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
  }
}

Signature scheme

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.

Event types (16)

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

Verify the signature

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)
})

Worked example: a pos.sale.completed consumer

A 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)
})

What's in data for pos.sale.completed

sale_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.

Shopping feeds Live

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.

Google XML sample

<?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 &amp; Vine</g:brand>
  </item>
</channel>
</rss>

The readiness rule

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.

Meta CSV columns

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.

CSV product import Live

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.

Template columns

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.

Accounting exports Live

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.

QuickBooks (3-column bank format)

Date, Description, Credit, Debit

Sales land in Credit; refunds land in Debit. Each row carries a truthful description with the payment reference.

Xero (bank statement format)

Date, Amount, Payee, Description, Reference

Refunds are negative amounts. Reference is the Stripe payment intent ID for reconciliation.

Provider connections Live

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:

“Connected” is earned

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.

Secrets stay masked

Keys are stored server-side and displayed only as ••••1234. They are never echoed back in full, in any response, ever.

Their bill, not ours

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.

Security model Live

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.

Honest roadmap

Things this page does not document as available, because they aren't yet:

Planned

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.

Planned

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.

Planned

Webhook automatic retries

Failed deliveries are logged visibly; retry-with-backoff is planned.

Planned

Live payments & hardware checkout

Stripe is in test mode during the pilot; live processing and one-click hardware ordering open after production approval.

Want to build against Hangerline?

See the integrations in action — including a live signature-verification demo that runs in your browser.

Explore the integrations showcase