hejj for developers
Your app, live on every new tab
hejj is the page people see each time they open their browser. With their consent, your app shows a live widget there. You send JSON; hejj draws it, beautifully, in every theme. Plain HTTP, any language, no SDK.
What people build
Pick one: the tile is what users see, the JSON is everything the app sends.
Northwind Bank
northwind.examplepullBalances and the latest transactions, without opening the bank.
hejj pulls a signed GET every 5 minutes while the tile is on screen.
GET https://northwind.example/hejj/finance?grant=gr_… Hejj-Signature: t=…,v1=… ← hejj → your app
How it works
A small JSON manifest at an https URL on your site: your app's name and the views your widget has. That URL is your app's id. No signup, no review.
{
"hejj": 1,
"name": "Northwind Bank",
"widgets": { "balance": {
"title": "Balance",
"endpoints": [{ "id": "total",
"mode": "pull",
"url": "https://northwind.example/hejj/total" }]
} }
}An “Add to hejj” button (OAuth 2 + PKCE) or a code they type at /link (device flow, for servers and scripts). They see your verified domain and exactly what the widget does.
→ /connect/app?client_id=…&widget=balance
&redirect_uri=…&code_challenge=…
← ?code=…&state=…
POST /api/apps/v1/token
← { access_token, signing_secret, grant_id }Push JSON blocks when your data changes, or let hejj pull your HTTPS endpoint (signed) while a tile is on screen. hejj draws them, in every theme and size.
POST /api/apps/v1/push/alerts
Authorization: Bearer hejj_wt_…
{ "v": 1, "blocks": [
{ "type": "status", "state": "ok",
"label": "All systems normal" } ] }Ten blocks, every theme
Data, not markup: no HTML, CSS, scripts or images. Colors are palette names, so they stay readable in light, dark and vanilla. On small tiles, the last blocks are dropped first, so put the important one on top.
metric · A headline number: money, count, percent or text, with a change.
metrics · A grid of up to 8 numbers.
sparkline · A trend line, oldest first.
list · Up to 30 rows with emoji, values, links and times.
table · Up to 5 columns and 20 rows.
progress · How far along, in any palette color.
status · ok, warn, down or unknown, with a detail line.
text · Plain text, up to 500 characters.
actions · Up to 3 buttons that call your app, signed.
empty · A calm first-run state.
The technical bits
| GET | /connect/app?… | Consent page (redirect flow) |
| POST | /api/apps/v1/device | Start the device flow |
| POST | /api/apps/v1/token | Exchange a code, or poll a device code |
| POST | /api/apps/v1/push/{endpoint} | Replace a push endpoint's data |
| GET | /api/apps/v1/grant | What this token may do |
| DELETE | /api/apps/v1/grant | Uninstall |
| POST | /api/apps/v1/token/rotate | New token + secret (old one: 24 h) |
| GET | your pull url?grant=… | hejj → you, signed (pull) |
| POST | your actions_url | hejj → you, signed (buttons) |
const [, t, v1] = /t=(\d+),v1=([0-9a-f]{64})/.exec(header) ?? [];
const body = createHash("sha256").update(raw).digest("hex");
const ok = Math.abs(Date.now() / 1000 - t) <= 300 &&
timingSafeEqual(Buffer.from(v1), Buffer.from(
createHmac("sha256", signingSecret)
.update(`${t}.${method}.${pathAndQuery}.${body}`)
.digest("hex")));Privacy by design. Your app never gets:
- · the user's name, email or hejj account
- · their IP address (hejj's server calls you)
- · their boards, notes or other widgets
- · time zone or language, unless they tick a box
It gets a random id per user, app and widget. Tokens are hashed, secrets sealed, and the user can disconnect your app in one click.
Build it with your AI agent
Copy this into Claude Code, Cursor or any coding agent, inside your app's project. It explains hejj and the whole API. Before writing any code, your agent reads your codebase and asks you, with its own suggestions:
- What the widget shows, and its title
- Its views, and pull or push for each
- Buttons, and which need a confirmation
- “Add to hejj” button or device code
- Time zone or language, if needed
- Your public URL and where the grant is stored
Then it builds the integration and checks a payload in the playground.
I want to show live data from this project in a hejj widget. Read this whole message, then ask me the questions in step 1 and wait for my answers before you write any code.
## What hejj is
hejj (https://hejj.net) is a browser homepage: people see it every time they open a new tab. It shows boards of links, notes, lists and widgets on a grid, works offline and syncs across devices. With a user's consent, another app (this project) can show one live widget on their boards. hejj draws the widget itself from JSON "blocks" the app sends: numbers, lists, sparklines, tables, progress bars, status lines and up to 3 buttons. The app never sends HTML, CSS, images or scripts. hejj never tells the app who the user is: the app gets a random grant id per user, app and widget, and ties it to its own user when the connect flow it started finishes. The user can disconnect the app at any time in hejj's Settings.
## 1. Ask me first
Look through this codebase, then ask me these in one message, numbered, each with the default you'd pick based on what you found:
1. What should the widget show, and to whom? Suggest a title (max 40 characters).
2. Which views should it have (1 to 8 "endpoints", e.g. "Balance", "Alerts")? For each: should hejj fetch it from this app while a tile is on screen (pull, every N seconds, at least 60), or should this app push new data whenever it changes (push)?
3. Should any view have buttons (up to 3) that do something in this app, like "Refresh" or "Mark as paid"? Which ones, and which need a confirmation first?
4. How should people connect: an "Add to hejj" button in this web app (redirect + PKCE), or a code they type at https://hejj.net/link (device flow, for servers, scripts and CLIs)?
5. Does any view need the user's time zone or language? Both are optional and unchecked by default on hejj's consent page, so the widget must work without them.
6. What is this app's public https origin, where should the grant (grant_id, access_token, signing_secret) be stored and encrypted, and which user record does it belong to?
## 2. Then build it with plain HTTP
There is no SDK: use this project's own HTTP client and crypto. Read the full spec and follow it exactly: https://hejj.net/developers/widgets.md. JSON Schemas: https://hejj.net/schemas/manifest-1.json and https://hejj.net/schemas/blocks-1.json. OpenAPI: https://hejj.net/schemas/apps-openapi-1.json.
- Manifest: JSON served with status 200 at an https URL on this app's origin, with no redirects. That URL is the app's client_id. Shape: {"hejj":1,"name","icon"?,"homepage"?,"redirect_uris" (redirect flow),"actions_url" (if there are buttons),"device_secret_sha256" (hosted device flow),"widgets":{"<key>":{"title","size"?,"context"?:["timezone","locale"],"endpoints":[{"id","title","mode":"pull"|"push","url"? (pull),"ttl"? (pull, 60 to 86400),"actions"?:["<id>"]}]}}}. Every URL hejj fetches (icon, actions_url, pull urls) must be on the manifest's host or a subdomain of it.
- Redirect flow: send the browser to https://hejj.net/connect/app?response_type=code&client_id=<manifest URL>&widget=<key>&redirect_uri=<listed in the manifest>&state=<random>&code_challenge=<base64url(sha256(code_verifier))>&code_challenge_method=S256. On the callback, check state, then within 60 seconds POST https://hejj.net/api/apps/v1/token with {"grant_type":"authorization_code","code","code_verifier","client_id","redirect_uri"}.
- Device flow: POST https://hejj.net/api/apps/v1/device with {"client_id","client_secret","widget"} (the sha256 of client_secret must equal the manifest's device_secret_sha256), or {"manifest":{...},"widget"} for an inline manifest (shown as unverified, push only). Show the user user_code and verification_uri, then every "interval" seconds POST https://hejj.net/api/apps/v1/token with {"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code"}. Keep polling on authorization_pending, wait 5 seconds longer on slow_down, stop on access_denied or expired_token.
- The token answer is {"access_token":"hejj_wt_...","signing_secret":"hejj_ws_...","grant_id":"gr_...","widget","endpoints","context"}. Store it encrypted against this app's user.
- Push: POST https://hejj.net/api/apps/v1/push/<endpoint id> with "Authorization: Bearer <access_token>" and a body {"v":1,"blocks":[...]}. 200 means it's shown; 422 lists every problem with its JSON path; 401 means the user disconnected, so delete the grant. Limits: 64 KB, 60 per minute, 1000 per day. Push when the data changes, not on a timer.
- Pull: hejj sends GET <the endpoint's url>?grant=<grant_id> (plus tz and locale only if the user allowed them) with a "Hejj-Signature: t=<unix seconds>,v1=<hex>" header. Answer 200 with blocks within 5 seconds, or 404 for an unknown grant.
- Buttons: hejj POSTs {"grant_id","endpoint","action","button_value"} to actions_url, signed the same way. Answer 200 with new blocks, 200 with {"toast":"..."}, or 204.
- Verify every request hejj sends: reject it unless |now - t| <= 300 and one v1 equals hex(HMAC-SHA256(key = signing_secret, message = "<t>.<METHOD>.<path and query exactly as received>.<hex sha256 of the raw body>")), compared in constant time. Test against the vector in the spec, section 7.
- Blocks: metric, metrics, sparkline, list, table, progress, status, text, actions, empty. Colors are palette names (like green or red), never hex. Money is in whole units, not cents. Put the most important block first: small tiles drop blocks from the end.
- Also available: GET https://hejj.net/api/apps/v1/grant (what the token may do), DELETE https://hejj.net/api/apps/v1/grant (uninstall), POST https://hejj.net/api/apps/v1/token/rotate (new token and secret; the old token works 24 more hours).
## 3. Before you finish
- Validate one real payload at https://hejj.net/developers/playground.
- Make sure a 401 from hejj deletes the stored grant.
- Tell me exactly how to try it end to end: how to connect, where the widget appears in hejj (Add, then Widget, then Connected apps), and how to trigger a push or a pull.