Harakumo Docs

One API key, your whole cloud. Every service through the SDK, the CLI, plain HTTPS or an AI assistant.

Topics
Platform
hk_live_••••FunctionsStorageDatabasesVector DBMemoryAgentsAI GatewayDeployDomainsPaymentsAuthMediaMailAnalyticsOne API key, your whole cloud
Start here

Getting started

Everything you create lives in a project, projects live in a workspace, and the workspace holds the plan, the Balance and the team. Every service is reachable four ways: the dashboard, plain HTTPS (https://harakumo.com/api/…), the JavaScript SDK and the CLI. An AI assistant connected over MCP uses the same API, with your permissions.

New here? The Quickstart builds a working app with a database, browser uploads and payments, step by step.

Quickstart
$ npm i @harakumo/sdk
$ export HARAKUMO_API_KEY=hk_live_...
> const hk = new Harakumo({ apiKey })
✓ your whole cloud, one client

The pieces

LevelWhat it is
AccountYou: an email address, a password and optional two-factor sign-in.
WorkspaceWhere billing and people live: one plan, one Balance, one team with roles. Signing up creates a personal workspace; you can create more and be invited to others.
ProjectOne app. It has its own address (https://<slug>.harakumo.app), environment variables and resources.
ResourceA database, bucket, function, auth pool, payment account, vector collection, memory store, agent or media asset inside a project.

Install

SDK (Node 18 or newer)
npm install @harakumo/sdk
CLI
npm install -g @harakumo/cli
harakumo login --key hk_live_…     # saved to ~/.harakumo/config.json
harakumo docs                      # these docs, in the terminal

Make your first call

  1. Create a key in Dashboard → Settings → API access (owners and admins can). It is shown once; store it as HARAKUMO_API_KEY.
  2. Send it as a Bearer token. The same key works for every service and every interface.
curl
curl https://harakumo.com/api/projects \
  -H "Authorization: Bearer $HARAKUMO_API_KEY"
SDK
import Harakumo from '@harakumo/sdk';

const hk = new Harakumo({ apiKey: process.env.HARAKUMO_API_KEY });
const { projects, org } = await hk.projects.list();

Keys are secrets: use them from servers, scripts and CI, never in a browser or a mobile app. For sign-in from a browser, auth pools have a publishable client id instead (see Auth pools).

Where to go next

  • Quickstart — signup to a live app with a database, storage and payments.
  • API keys & authentication — full, read-only and project keys, and which to give an app.
  • Limits & scaling — every number, and how each service behaves under load.
  • Errors & rate limits — what each status and code means.
  • AI connector (MCP) — run all of it from Claude or ChatGPT.

Quickstart: your first app

You will build a small notes app: an Express server that stores notes in an edge SQLite database, lets the browser upload attachments straight to object storage, and sells an upgrade through a hosted checkout. It deploys as a server app at https://<slug>.harakumo.app.

Replace the values in angle brackets with the ones each step prints.

1. Sign up and create a setup key

  1. Sign up at harakumo.com/signup and enter the 6-digit code emailed to you. Verify now: live payment links need a verified workspace-owner email.
  2. Open Dashboard → Settings → API access and create a key with full access to the whole workspace. It stays on your machine for setup.
Terminal
npm install -g @harakumo/cli
export HARAKUMO_API_KEY=hk_live_…          # the key you just created
harakumo login --key $HARAKUMO_API_KEY

2. Create the project, a database, a bucket and a payment account

Terminal
harakumo projects create notes-app       # ✓ Created project notes-app #<projectId>
harakumo db create <projectId> notes     # ✓ Creating notes (sqlite, #<dbId>)
harakumo db query <dbId> "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL, attachment TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP)"
harakumo storage create <projectId> uploads
harakumo storage <projectId>             # lists buckets with their ids: note <bucketId>

# Payments are live as soon as they are enabled:
curl -X POST https://harakumo.com/api/projects/<projectId>/payments \
  -H "Authorization: Bearer $HARAKUMO_API_KEY"
# → { "account": { "id": <accountId>, … } }

3. Give the app its own key

Your setup key can do anything in the workspace; the deployed app should not. In Settings → API access create a second key with full access and Project set to notes-app. It works on this project's database, buckets and payment account and is refused everywhere else.

Store it and the resource ids as the project's environment variables — in the dashboard (project → Environment) or with the API:

VariableValue
HARAKUMO_API_KEYthe project key (hk_live_…)
NOTES_DB_ID<dbId>
UPLOADS_BUCKET_ID<bucketId>
PAYMENTS_ACCOUNT_ID<accountId>
APP_URLhttps://<slug>.harakumo.app (the slug is on the project page)
One variable with curl
curl -X POST https://harakumo.com/api/projects/<projectId>/env \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{"key":"NOTES_DB_ID","value":"<dbId>"}'

Environment variables are copied into the app when it deploys. After changing one, redeploy.

4. Write the app

Three files in a folder called notes-app. Keep the page in web/: an index.html in the folder root, or in public/, dist/, build/, out/, _site/, site/ or docs/, makes the pipeline serve the folder as a static site instead of starting your server.

package.json
{
  "name": "notes-app",
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": { "@harakumo/sdk": "latest", "express": "^4.21.2" }
}
server.js
import express from 'express';
import Harakumo from '@harakumo/sdk';

const hk = new Harakumo({ apiKey: process.env.HARAKUMO_API_KEY });
const DB = Number(process.env.NOTES_DB_ID);
const BUCKET = Number(process.env.UPLOADS_BUCKET_ID);
const PAYMENTS = Number(process.env.PAYMENTS_ACCOUNT_ID);
const APP_URL = process.env.APP_URL;

const app = express();
app.use(express.json());
app.use(express.static('web'));

// Notes live in the edge SQLite database; every query is one HTTPS call.
app.get('/api/notes', async (req, res) => {
  const { results } = await hk.databases.query(DB,
    'SELECT id, body, attachment, created_at FROM notes ORDER BY id DESC LIMIT 50');
  res.json(results);
});

app.post('/api/notes', async (req, res) => {
  const { body, attachment = null } = req.body;
  await hk.databases.query(DB, 'INSERT INTO notes (body, attachment) VALUES (?, ?)', [String(body), attachment]);
  res.status(201).json({ ok: true });
});

// The browser uploads straight to storage with a signed URL; the bytes never touch this server.
app.post('/api/upload-url', async (req, res) => {
  const { filename, contentType } = req.body;
  const key = 'attachments/' + Date.now() + '-' + String(filename).replace(/[^a-zA-Z0-9._-]/g, '_');
  const { url } = await hk.storage.presign(BUCKET, { key, method: 'put', contentType });
  res.json({ url, key });
});

// A hosted checkout. The buyer comes back to /thanks?session_id=…
app.post('/api/checkout', async (req, res) => {
  const { url } = await hk.payments.createSession(PAYMENTS, {
    amountCents: 500,
    description: 'Notes Pro',
    reference: 'user-' + String(req.body.userId || 'anonymous'),
    successUrl: APP_URL + '/thanks',
    cancelUrl: APP_URL + '/',
  });
  res.json({ url });
});

// Confirm on the server; never trust the redirect alone.
app.get('/thanks', async (req, res) => {
  const r = await fetch('https://harakumo.com/api/payments/' + PAYMENTS + '/sessions/'
    + encodeURIComponent(req.query.session_id || ''), {
    headers: { authorization: 'Bearer ' + process.env.HARAKUMO_API_KEY },
  });
  const { paid, payment } = await r.json();
  res.send(paid ? 'Paid $' + (payment.amount_cents / 100).toFixed(2) + ' — thank you!' : 'Payment not completed yet.');
});

app.listen(process.env.PORT || 3000);
web/index.html
<!doctype html>
<title>Notes</title>
<form id="note"><input name="body" placeholder="Write a note" required> <input type="file" name="file"> <button>Save</button></form>
<ul id="list"></ul>
<button id="buy">Upgrade for $5</button>
<script type="module">
const json = (url, body) => fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());

async function load() {
  const notes = await (await fetch('/api/notes')).json();
  document.getElementById('list').replaceChildren(...notes.map(n => {
    const li = document.createElement('li');
    li.textContent = n.body + (n.attachment ? ' (attachment)' : '');
    return li;
  }));
}

document.getElementById('note').onsubmit = async (e) => {
  e.preventDefault();
  const form = new FormData(e.target);
  const file = form.get('file');
  let attachment = null;
  if (file && file.size) {
    const type = file.type || 'application/octet-stream';
    const { url, key } = await json('/api/upload-url', { filename: file.name, contentType: type });
    await fetch(url, { method: 'PUT', headers: { 'content-type': type }, body: file });  // same Content-Type as signed
    attachment = key;
  }
  await json('/api/notes', { body: form.get('body'), attachment });
  e.target.reset();
  load();
};

document.getElementById('buy').onclick = async () => {
  location.href = (await json('/api/checkout', {})).url;
};
load();
</script>

5. Deploy

Terminal
harakumo deploy <projectId> ./notes-app    # uploads the folder
harakumo deploys <projectId>               # queued → building → deploying → ready

Deploy the folder without node_modules: an upload is at most 25 MB, and the build machine installs dependencies itself. Keep secrets in environment variables, not in a .env file.

What happens

The pipeline finds a package.json with a start script and no static index.html, installs the dependencies on a build machine and runs npm start in a container. The first deploy takes a few minutes. When the status is ready, open https://<slug>.harakumo.app.

  • A server app listens on process.env.PORT; the platform sets it.
  • An idle app sleeps after 15 minutes. The next visitor sees a short starting page while it wakes.
  • The container's disk is wiped on sleep and on every deploy. Keep data in the database and storage, as this app does.

6. Take a payment

Press Upgrade on the live app. The hosted checkout charges a real card: to try the flow, pay yourself and refund it from Dashboard → Payments (refunds are for the workspace owner). You return to /thanks?session_id=…, and the server confirms the payment with the session lookup.

The payment appears in Dashboard → Payments as Earnings: $5.00 less the 3.9% + 30¢ fee. Each payment is held 7 days before the workspace owner can withdraw it.

Next

  • Databases — batch statements, export, limits and errors.
  • Storage — download links, listings and folders.
  • Payments — webhooks, metadata and refunds.
  • Auth pools — add sign-in for your users.
  • Limits & scaling — what this app can handle, and where the walls are.

API keys & authentication

Send the credential in the Authorization header: Authorization: Bearer hk_live_…. The same header works for REST, the SDK, the CLI and the MCP connector.

Owners and admins create keys in Dashboard → Settings → API access. A key is shown once and stored only as a hash; it does not expire, and revoking it takes effect immediately.

Kinds of key

KeyActs asCan
Full, whole workspacedeveloperCreate, change and delete resources in every project, deploy, send mail, use AI, buy domains from the Balance. It can also delete projects.
Full, one projectdeveloperThe same, but only on that project's endpoints. Workspace-wide endpoints refuse it.
Read-only, whole workspaceviewerGET requests, plus reads sent as POST (vector query, memory search, storage download links). No environment variables, no SQL, no uploads.
Read-only, one projectviewerReads on that project only.

No API key can manage billing, members or other keys, or move money out: withdrawals, refunds and payout setup need the workspace owner signed in to the dashboard.

Which key to give what

UseKey
Your laptop, CI, setup scriptsFull, whole workspace
A deployed app that uses its database, storage, payments, vectors, memory or auth poolFull, one project
A deployed app that also sends mail or calls the AI GatewayFull, whole workspace (those endpoints are workspace-wide and refuse project keys today)
Dashboards, monitoring, read-only AI assistantsRead-only

Where a project key works

A project key passes every route that names its project: /api/projects/<id>/…, and item routes whose resource belongs to it — /api/databases/:id, /api/storage/:id, /api/functions/:id, /api/vectors/:id, /api/memory/:id, /api/agents/:id, /api/payments/:accountId, /api/auth-pools/:id, /api/deployments/:id.

It is refused, with 403 "This API key only works on project N", on workspace-wide routes: listing projects, /api/mail/send, /api/ai/*, /api/domains/*, /api/repos/*, /api/usage and billing.

Create a key with the API

curl (an owner or admin session or key)
curl -X POST https://harakumo.com/api/orgs/<workspaceId>/api-keys \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "name": "notes-app production", "access": "full", "projectId": 12 }'
# 201 → { "key": { "id", "prefix", "access", "project_id" }, "secret": "hk_live_…" }   (secret shown once)

Creating keys needs the admin role, so an API key (developer or viewer) cannot mint more keys. Use the dashboard, or an admin's connector.

Other credentials

CredentialWhere it comes fromActs as
Browser sessionSigning in to the dashboard; an httpOnly cookie, 30 daysYou, with your role in the chosen workspace
Connector token (hk_mcp_…)Connecting Claude, ChatGPT or another MCP client (OAuth)You, read-only or read-and-manage, in one workspace or all of them
Auth-pool client secret (as_…)Enabling an auth poolOnly that pool's sign-in actions — never anything else
Auth-pool client id (ap_…)Enabling an auth poolPublishable: browser sign-in for that pool, when turned on

Choosing a workspace

A key belongs to one workspace. A session or an all-workspaces connector acts in the workspace named by ?org=<id> or the x-harakumo-org header, else the one last chosen in the dashboard.

When a key is refused

StatusMessageWhat to do
401UnauthorizedThe header is missing, or the key was revoked or mistyped.
403This API key is read-onlyUse a full key for writes.
403This API key only works on project NUse a key for that project, or a workspace key for workspace-wide routes.
403Your role (developer) does not allow this actionThe action needs an admin or owner (members, keys, billing).
404Not foundThe id does not exist or belongs to another workspace — the two look the same on purpose.

Reference

SDK
import Harakumo from '@harakumo/sdk';
const hk = new Harakumo({ apiKey: process.env.HARAKUMO_API_KEY });   // baseUrl defaults to https://harakumo.com
// Errors throw HarakumoError with .status and .body (the JSON the API sent).

Limits & scaling

Plans are per workspace and limit how many things you create and the monthly allowances. The technical limits below apply on every plan. Data size, storage bytes, bandwidth and function traffic have no plan quota yet — they are listed as not metered.

Plans

HobbyProEnterprise
Price (per workspace)Free$20 a month$500 a month
AI usage included$1 a month (personal workspace only)$10 a month$100 a month
Domains includedNone1 a year (up to $15 each)5 a year (up to $15 each)
Emails (rolling 30 days)1005,000100,000
Custom sending domains11050
Projects3201,000
Functions5501,000
Databases (10 GB each)110100
Storage buckets220500
Vector collections110200
Memory stores220500
Agents220500
Auth pools110100
Payment accounts110100
Media assets51001,000
Repos550500
Custom domains connected220500
Team members310100
Workspaces you can own210100
AI requests a minute606006,000
Agent runs at once1520

Past a count the create call answers 402 with code plan_limit, the limit and the current count. Nothing existing is deleted when a plan changes; only new creates are refused.

Not metered or capped yet

  • Requests and bandwidth to sites and server apps, and traffic to function URLs.
  • Storage bytes (measured daily and shown in Analytics, but no quota).
  • Database size (measured daily; each database stops at the engine's 10 GB).
  • Server-app running time, errors and latency.

Databases

LimitValue
Size of one database10 GB
One row or value2 MB
One SQL statement100 KB
Bound parameters per statement100
Time per query30 seconds
Columns per table100
Statements in one batch100
Query requests per workspace600 per 5 minutes (a batch counts once)

Deploys and server apps

LimitValue
One upload (folder, tar.gz or JSON files)25 MB compressed, 40 MB unpacked
One file10 MB
Files in one upload5,000
Files a static site serves500
Git repository download20 MB compressed, no file-count cap
Build timeabout 13 minutes
Server appone container per app (about a quarter of a CPU and 1 GB of memory), no extra copies
Server app sleepafter 15 idle minutes; disk wiped on sleep and on every deploy
Awake server apps40 across the whole platform today; sleeping apps do not count

Functions, storage and repos

LimitValue
Function codeone ES module, at most 200 KB, no npm install
Test invokefirst 16 KB of the response, 10-second timeout
One storage upload5 GB (one signed PUT)
Object key1,024 characters
Signed link lifetime60 seconds to 7 days (default 15 minutes)
Objects per listing page1,000 (default 100), with a cursor for the next page
Repo push over REST500 files, 10 MB per file, 25 MB per push

AI, vectors, memory and agents

LimitValue
Gateway input200,000 characters (system prompt plus messages), up to 500 messages
Gateway outputmax_tokens up to 8,192 (2,048 on edge models); default 1,024
AI requests a minute60 hobby, 600 pro, 6,000 enterprise — chat and embeddings counted separately
Embeddings per call100 inputs
Vector dimensions384, 768 (default), 1,024 or 1,536
Vectors per upsert1,000 (100 when sending text to embed)
Vector query topK100, or 50 with full metadata or values
Memory value / key1 MB / 512 characters
Conversation window1,000 most recent messages per scope
Memory quotasitems per store and bytes per workspace, by plan; a write past them gets 402
Agent steps1–24 per run (default 8)
Agent runs6 a minute per workspace; input up to 32,000 characters
Media uploadvideo only, up to 1 hour

Mail, payments and auth pools

LimitValue
Email per callone recipient; text and/or HTML
Email quota100 hobby, 5,000 pro, 100,000 enterprise per rolling 30 days; optional daily cap of your own
Custom sending domains1 hobby, 10 pro, 50 enterprise per workspace (pending and failed domains count until removed)
Checkout amount$0.50 to $1,000,000, USD, one-time
Checkout linksingle use, expires after 24 hours
Earnings hold7 days per payment before it can be withdrawn
End-user password8–256 characters; 10 wrong tries lock the account for 15 minutes
End-user tokenHS256 JWT, valid 7 days
Browser sign-up10 per IP an hour, 600 per pool an hour

Rate limits

Every 429 carries code, a Retry-After header and retry_after seconds in the body. API quotas also send X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Back off for Retry-After seconds and try again.

WhatLimitCode
AI chat60 / 600 / 6,000 a minute (hobby / pro / enterprise)ai_rate_limited
Embeddings, memory search, text sent to vectorsa separate bucket of the same sizeai_rate_limited
Database queries, vector calls, semantic memory600 calls per 5 minutes per workspace, shared (a batch of statements is one call)rate_limited
Agent runs6 a minute per workspacerate_limited
Balance top-ups10 an hourrate_limited
New workspaces5 a day per personrate_limited
Emailyour optional daily capdaily_cap

How it scales

ServiceAt millions of users today
Static sitesScale: served from the edge cache close to each visitor, the rest from object storage.
FunctionsScale: each request runs at the edge location nearest the caller, with no servers to size.
StorageBytes scale: uploads and downloads go straight to object storage. Each signed link is one API call to Harakumo, so sign links with a longer expiresIn and reuse them rather than signing one per page view.
Auth poolsVerify tokens in your app with the JWT secret (no call per request). Sign-ups and logins run on the shared platform and suit apps with thousands of active users.
DatabasesEach database has one writer and stops at 10 GB, and each query is an HTTPS call through Harakumo capped at 600 requests per 5 minutes per workspace. Good for development, internal tools and low-traffic apps; not yet for a high-traffic public app. Batch statements, cache reads and split data across databases.
Server appsOne small container per app with no extra copies, a cold start after it sleeps, and a platform-wide cap on awake apps. For large traffic, put the front end on a static site and the server logic in functions.
AIModel capacity scales with the vendors; your workspace is capped by the per-minute limits above.
MailUp to the plan's 30-day quota, one recipient per call.

Coming: databases reached directly from your functions and apps (no HTTPS hop, no shared limit), read replicas, a Postgres engine for datasets over 10 GB, extra copies and larger sizes for server apps, and metered traffic.

Errors & rate limits

Errors come back as { "error": "…", "code"?: "…", …details }. The sentence says what happened and what to do; the code is stable for programs. Nothing is charged for a refused request.

Status codes

StatusMeaning
400The request is wrong: a missing field, a bad value, or a SQL mistake (the database's own message, cleaned).
401No credential, or it is invalid or revoked.
402Money or plan: a plan count reached, the Balance or AI credit too low, or the spending limit reached.
403The credential may not do this: role, read-only key, project key, owner-only money action, a paused or unverified account.
404Not found — or it belongs to another workspace.
409The resource is in the wrong state: still being created, disabled, or a delete blocked by money or work in flight.
410A database that no longer exists on the edge.
413Too large: an upload over the size cap, or AI input over 200,000 characters.
422Refused on purpose: a suppressed email address, or an AI answer that used the whole token budget with nothing to show.
429Too many requests. Wait Retry-After seconds.
501Not available yet (for example, a Postgres database or an image media asset).
502Something upstream failed: a delivery provider refused, or a delete could not finish (it is safe to retry).
503Temporarily unavailable. Retry after the Retry-After header when there is one.

Codes

CodeStatusMeaning and fields
plan_limit402A plan count or the email quota is reached. limit, current.
insufficient_balance402The Balance does not cover a purchase. price_usd, balance_usd, shortfall_usd, add_funds_url, card_on_file.
ai_credits_exhausted402Included AI and the Balance are used up. available_usd, resets_on, buy_url.
spend_limit_reached402The workspace's monthly spending limit would be passed. spend_limit_usd, spent_this_month_usd, limits_url.
email_unverified402 / 403The workspace owner's email must be verified (Hobby vendor AI models, live payment links).
payouts_require_owner403Withdrawals, refunds and payout setup need the owner signed in to the dashboard.
origin_not_allowed403An auth pool refused a browser Origin that is not on its list.
browser_access_off403An auth pool's browser sign-in is turned off.
mail_paused / mail_suspended403Sending is paused by an admin, or suspended by the platform.
invalid_domain400A sending domain that is not a real domain you could own (a single word, a test domain, a Harakumo name).
invalid_from / invalid_reply_to400A From or Reply-To address that is not allowed; the sentence says which rule.
from_domain_not_added400A from address on a domain that is not a sending domain of this workspace. Add it first.
domain_claimed_elsewhere / domain_taken409The domain belongs to another workspace on Harakumo.
payments_disabled409The payment account is disabled; re-enable it on the project.
deletion_blocked409A delete is refused. blockers[] has one sentence per reason (earnings, a withdrawal, an open checkout, a running deploy, bought domains).
confirm_forfeit_required409Deleting a workspace would give up a positive Balance; send confirmForfeit: true to accept.
suppressed422The recipient is on the suppression list.
rate_limited429A rate limit. retry_after; API quotas add limit and window_seconds.
ai_rate_limited429The AI per-minute limit. limit, window_seconds, retry_after_seconds.
daily_cap429The workspace's own daily email cap.
delivery_failed502The mail provider refused the message; message has the reason.
provider_error502A sending domain could not be set up; nothing was saved. Try again in a minute.
teardown_incomplete502A delete left some resources behind; the project is kept as teardown_failed. Delete again to retry.
processor_errorvariesThe payment processor refused; the sentence says why.

Handling errors in code

SDK
import Harakumo, { HarakumoError } from '@harakumo/sdk';

try {
  await hk.databases.query(dbId, 'SELECT 1');
} catch (err) {
  if (err instanceof HarakumoError) {
    if (err.status === 429) { /* wait err.body.retry_after seconds, then retry */ }
    if (err.body?.code === 'plan_limit') { /* show err.message to the person */ }
  }
  throw err;
}

Architecture

Harakumo is an app's whole backend behind one client. Your front end and server deploy to the Harakumo edge; every capability the app needs is a namespace on the same SDK or a path on the same API, already wired together. The diagram shows a real shape — an online store, end to end — with the role each service plays.

Your server holds the API key; browsers get only what your server hands them (signed upload and download URLs, checkout links) or the auth pool's publishable client id.

System design — an online store on Harakumo
usersyour appfrontend + server · deployed from Git or upload · auto-sleep & wakemy-store.harakumo.appliveconst hk = new Harakumo({ apiKey })Databasesproducts & ordershk.databasesStorageproduct imageshk.storageAuthcustomer login · JWThk.authPaymentscheckout & payoutshk.paymentsVector DBsemantic searchhk.vectorsMemorycarts & sessionshk.memoryAI Gatewaysupport copilothk.aiMailorder receiptshk.mailevery arrow is one SDK call — no service accounts, no glue infrastructure
Build & deploy

Projects & environment

Each project gets a permanent slug, which becomes its address: https://<slug>.harakumo.app. Creating a project reserves the name; nothing serves until the first deploy succeeds. A project that only holds a database or storage has no website, and that is fine.

Projects
project: my-appfunctionsdbstorageenv varsdeploysmy-app.harakumo.appone project holds everything your app needs

What a project reports

GET /api/projects/:id returns project (with live_url, null until a deploy is live), serving (state: live, down, deploying, failed or not_deployed, plus the serving deployment), deployAction (what Deploy does: rebuild the latest GitHub commit, or redeploy kept files) and services (counts of vectors, memory stores, agents, auth pools and media, and the payment account). The project id is on the project's Overview (Connect your app) and Settings.

Environment variables

  • Set them in the dashboard (project → Environment) or with POST /api/projects/:id/env { key, value }. A key is letters, digits and underscores; setting an existing key replaces it.
  • They are copied into sites, server apps and functions when those deploy. Change one, then redeploy (or press Redeploy) for it to take effect.
  • Only the production target is used.
  • Values are readable by developers and above, never by viewers or read-only keys. They are not encrypted secrets — do not reuse a value you could not afford a teammate to see.

Deleting a project

Deleting removes the site, functions, buckets and their files, databases, vector collections, memory stores, media and custom-domain connections. Domains bought in the workspace stay in the workspace and keep renewing.

  • Refused with 409 deletion_blocked while the project holds unwithdrawn earnings, a pending withdrawal, a checkout link that can still be paid, or a running deploy. blockers[] says what to do.
  • If something cannot be removed, the answer is 502 teardown_incomplete; the project is kept with status teardown_failed, accepts only reads and deletes, and deleting again retries.
  • Deleting needs an admin, or a full workspace key. Project keys and read-only keys cannot delete projects.

Reference

SDK
const { project } = await hk.projects.create({ name: 'my-app' });
const { project: p, serving, services } = await hk.projects.get(project.id);   // p.live_url, serving.state
const { projects } = await hk.projects.list();
await hk.projects.delete(project.id);

Deployments

Three ways in, one pipeline: a GitHub repository (when you press Deploy or call the API), an upload (dashboard, CLI, SDK or files sent by an AI assistant), or a commit in Harakumo Repos. A failed deploy never replaces a working one.

Deploy pipeline
Git repoUpload (folder / tar.gz)classifystatic outputbuild scriptNext.js / Node serverHarakumo edgebuild machinenpm ci + buildapp containernpm startsite liveapp live<slug>.harakumo.applive
queued→building→deploying→ready
GitHub optional — both transports feed the same pipeline

How your code is recognized, in order

  1. A worker.js (or _worker.js) at the root becomes an edge program with your production environment variables.
  2. A Next.js app that is not a static export becomes a server app.
  3. An index.html in the root or in dist/, build/, out/, _site/, public/, site/ or docs/ becomes a static site. (From Git, a repository with a build script only serves dist/, build/, out/ or _site/.)
  4. A package.json build script (Vite, Astro, Create React App …) builds on a build machine; the output becomes a static site.
  5. A package.json start script (Express and friends) becomes a server app.

A server app must not carry a static index.html in the places step 3 looks, or it is served as a static site. Keep its pages in another folder.

Static sites

  • Served from the edge cache (the workspace's CDN setting), then object storage. They scale with traffic.
  • Up to 500 files per site.
  • Dotfiles (.env, .git …) and key files (.pem, .key, id_rsa …) are never served; /.well-known/ is.

Server apps

  • Run with npm start (or next start) on Node 22 in one container: about a quarter of a CPU and 1 GB of memory, one copy.
  • Listen on process.env.PORT. Production environment variables are injected at deploy.
  • After 15 idle minutes the app sleeps; the next visitor sees a short starting page while it wakes (usually seconds, longer on a cold start).
  • The disk is wiped on every sleep and deploy: keep uploads and data in Storage and Databases.
  • If the app crashes three times, the project shows App crashed with the app's own output, and visitors get a plain 503 page.
  • At most 40 server apps are awake across the platform today; sleeping apps do not count.

From GitHub

  • Set the repository once (at project creation or PATCH /api/projects/:id { repoUrl, repoBranch }). Only github.com repositories are supported.
  • Deploys run when you press Deploy or call the API. A push to GitHub does not deploy on its own yet.
  • Only the production branch deploys. A different branch is refused unless repoUrl is set in the same call, which makes that branch production.
  • Private repository: add a GITHUB_TOKEN environment variable to the project (a fine-grained token with read access to that repository). It is used only for that project.

From an upload

  • Up to 25 MB compressed, 40 MB unpacked, 10 MB per file and 5,000 files.
  • Leave out node_modules and build output: the build machine installs and builds. The dashboard leaves node_modules, .next, .git, .vercel, .turbo, .cache and .env files behind for you.
  • Put secrets in environment variables, never in an uploaded .env file.
  • A key limited to the project can deploy it; a read-only key can read deployments and logs.

Redeploy and roll back

POST /api/deployments/:id/redeploy deploys the same source again as a new deployment: the kept files of an upload (the serving deployment's files are kept) or a GitHub deployment's commit. Redeploying an older GitHub deployment is a rollback. This is also how environment variable changes take effect.

Status and logs

A deployment moves queued → building → deploying → ready, or failed with the reason in its logs. Server apps also report runtime status (running, crashed or failed) and recent output in runtime_status and runtime_logs.

Deploy a folder and wait for the URL (SDK)
const { deployment } = await hk.deployments.uploadDir(projectId, './dist');
let d = deployment;
while (!['ready', 'failed'].includes(d.status)) {
  await new Promise(r => setTimeout(r, 3000));
  ({ deployment: d } = await hk.deployments.get(deployment.id));
}
console.log(d.status, d.preview_url);

Reference

SDK
await hk.projects.update(project.id, { repoUrl: 'https://github.com/you/repo', repoBranch: 'main' });
const { deployment } = await hk.deployments.create(project.id);            // latest commit on the production branch
const { deployment: d } = await hk.deployments.get(deployment.id);        // status, logs, preview_url

const { deployment: up } = await hk.deployments.uploadDir(project.id, './my-app');   // Node: a folder
// await hk.deployments.uploadArchive(project.id, tarball);                // a gzip tar (Buffer, Uint8Array or Blob)

Functions

A function is one ES module with a default fetch handler, deployed to the edge. Each request runs at the location nearest the caller with no server to size or keep awake. Create one with code, or without code to get a starter handler you replace later.

Edge functions
worker.js~0ms cold startdeployed to the Harakumo edge in seconds

The address

Every function is served at https://hk-<project-slug>--<function-name>.harakumo.app — never under the project's own site address. The create response carries it as function.url. A function created before the move to harakumo.app moves on its next redeploy, and its old address stops.

Runtimes

runtimeWhat the code gets
node22 (default), node20Web APIs plus the Node.js compatibility layer: node:crypto, Buffer, process.env.
edgeWeb APIs only: fetch, Request and Response, crypto.subtle, streams.

Environment variables

  • The project's production variables are available as env.NAME in the handler.
  • They are read when the function deploys. After changing one, redeploy the function with its code (PUT /api/functions/:id).
  • They are visible to anyone who can read the project's environment; they are configuration, not a vault.

Limits

  • One module, at most 200 KB, no npm install and no bundler — bundle dependencies yourself if you need them.
  • Code that does not parse, or imports something that does not exist, is refused with 400 and the edge's reason; nothing is created.
  • Invoking from the API or dashboard is a test call: it forwards method, path, headers and body, waits up to 10 seconds and returns the status, headers and the first 16 KB of the body. Real traffic goes to the URL and is not counted or capped yet.
  • Developers and above can read a function's stored source; viewers cannot.
  • Keys: a key limited to the project can create, deploy and delete its functions; a read-only key can read and test-invoke them.

Reference

SDK
const code = `export default {
  async fetch(request, env) {
    return Response.json({ hello: 'world', region: env.REGION ?? 'edge' });
  },
};`;
const { function: fn } = await hk.functions.create(project.id, { name: 'hello', runtime: 'node22', code });
console.log(fn.url);                            // https://hk-<project>--hello.harakumo.app
await hk.functions.update(fn.id, { code });     // redeploy (also picks up env changes)

Repos

Repos need no outside account or token. Every push saves a full snapshot as commit 1, 2, 3 …; deploying an older number is a rollback. An assistant connected over MCP can create a repo, push what it wrote and put a commit live in one conversation (create_repo, push_files, get_repo_files, deploy_repo, list_repos).

Limits

  • A push over REST: up to 500 files, 10 MB per file, 25 MB in total. One push_files tool call is smaller (300 files of about 600 KB), so large projects push over REST or in parts.
  • Text files up to 512 KB read back one by one; larger and binary files are listed with their size. Binary files are pushed base64-encoded ("base64": true).
  • Environment files (.env and friends) are never readable back through the API. Do not commit secrets.
  • Repos are workspace-wide: project keys cannot use them.

Reference

SDK
// Repos are reached over REST or the AI connector; the SDK has no repo methods yet.
Data

Databases

Every database you create is a separate SQLite database on the edge — two databases never share a file or a table. Engine "sqlite" is the one available today; Postgres, MySQL and Redis answer 501 until they exist.

There is no host and port: every query is a POST https://harakumo.com/api/databases/:id with an API key. Keep the key on your server.

Databases
SELECT * FROM users;idemailstatus1ada@example.comactive2grace@example.comactive3linus@example.cominvitedPOST /queryEdge SQLiteplain SQL over HTTP — no drivers, no connection pools

Connect from your app

  1. Create the database (dashboard, CLI or POST /api/projects/:id/databases { name }) and note its id. The database page's Connect tab shows the endpoint, the id and snippets with your real values.
  2. Create an API key limited to the project (Settings → API access; full access, Project set). Queries need full access, even for SELECT.
  3. Put the key and the database id in the project's environment variables (for example HARAKUMO_API_KEY and NOTES_DB_ID) and redeploy.
  4. Query from server code: the SDK, fetch or curl.
fetch
const res = await fetch('https://harakumo.com/api/databases/' + process.env.NOTES_DB_ID, {
  method: 'POST',
  headers: { authorization: 'Bearer ' + process.env.HARAKUMO_API_KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ sql: 'SELECT * FROM notes WHERE id = ?', params: [42] }),
});
const { results, meta } = await res.json();   // results: rows of the last statement
curl
curl -X POST https://harakumo.com/api/databases/5 \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "sql": "SELECT count(*) AS n FROM notes" }'

Responses

  • A single request answers { results, statements, meta }. SQL holding several statements runs them in order; results holds the last statement's rows and statements how many ran.
  • meta has the rows read and written, changes and last_row_id from the database.
  • Use ? placeholders with params for every value — never build SQL by joining strings.

Batch

Send up to 100 statements, each with its own params, in one request. It counts once toward the workspace rate limit, so batching is the way to do bulk work. Do not rely on a failing statement rolling back the earlier ones in the same batch until that behavior is confirmed; design writes to be safe to retry.

Batch
curl -X POST https://harakumo.com/api/databases/5 \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "batch": [
        { "sql": "INSERT INTO notes (body) VALUES (?)", "params": ["first"] },
        { "sql": "INSERT INTO notes (body) VALUES (?)", "params": ["second"] },
        { "sql": "SELECT count(*) AS n FROM notes" } ] }'
# → { "batch": [ { "results": [], "meta": {…} }, …, { "results": [{ "n": 2 }], "meta": {…} } ] }

Export

POST /api/databases/:id/export starts an export and answers { status: "active", bookmark } while it runs; call again with the bookmark until it answers { status: "complete", download }. GET /api/databases/:id/export?bookmark=… returns the .sql file (schema and data), which any SQLite tool can load. The Connect tab has an Export button.

There is no point-in-time restore or import through Harakumo yet. Export regularly if the data matters, and treat delete as permanent.

Limits

LimitValue
Size of one database10 GB
One row or value2 MB
One SQL statement100 KB
Bound parameters per statement100
Time per query30 seconds
Columns per table100
Statements per batch100
Query requests per workspace600 per 5 minutes
Databases per plan1 hobby, 10 pro, 100 enterprise

Errors

StatusMeaning
400A SQL mistake; the message is the database's own.
409The database is not available (its create failed, or it was never created). A failed create shows the reason; retry with PATCH { "action": "retry" } — it does not count against the plan.
410The database no longer exists on the edge.
429The workspace rate limit or the database service is throttling. Wait Retry-After seconds, and batch.
502 / 503A platform problem. Retry shortly.

How it scales

Each database has one writer and stops at 10 GB, and every query is an HTTPS call through Harakumo, capped at 600 requests per 5 minutes per workspace. That suits development, internal tools and low-traffic apps. For more: batch statements, cache reads in your app, and split data across several databases (per customer, per region). Direct connections from your functions and apps, read replicas and a Postgres engine for single datasets over 10 GB are planned.

Moving from MongoDB

@harakumo/docdb keeps the MongoDB and Mongoose call shapes (find, updateOne, populate, bulkWrite …) and stores documents as JSON in a Harakumo database, so an app moves without rewriting every query. It is in preview and not yet published to npm; ask support for access.

Reference

SDK
const { database } = await hk.databases.create(project.id, { name: 'app', engine: 'sqlite' });
const { results } = await hk.databases.query(database.id, 'SELECT * FROM users WHERE id = ?', [42]);
// Batches and export are REST calls (below); the SDK sends single statements.

Storage

Files live in buckets; each bucket belongs to a project and is its own bucket in object storage. A file's key is its path (avatars/42.png); folders are just the part of the key before a slash.

Files are private. To upload or download, your server asks Harakumo for a signed URL for one key, and the client sends or fetches the bytes directly. There are no public bucket URLs and no S3-style access keys yet.

Storage
presignsigned URLbytes go direct — zero egressyour app12Harakumo APIedge storagepresign once — uploads never touch your servers

Upload from the browser

  1. Your server signs a PUT for a key, naming the Content-Type.
  2. The browser PUTs the file to that URL with the same Content-Type header. Buckets accept browser uploads from any origin.
Server
const { url, expiresAt } = await hk.storage.presign(bucketId, {
  key: 'avatars/' + userId + '.png', method: 'put', contentType: 'image/png',
});
// hand url to the browser
Browser
await fetch(url, { method: 'PUT', headers: { 'content-type': 'image/png' }, body: file });

Download links

Sign a GET for a key and send the person (or an img tag) to the URL. Links last 15 minutes by default; pass expiresIn from 60 seconds to 7 days (604800). For files shown to many people, sign a longer link once and reuse it instead of signing on every view.

curl
curl -X POST https://harakumo.com/api/storage/7 \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "key": "reports/q3.pdf", "method": "get", "expiresIn": 86400 }'
# → { "url": "…", "expiresIn": 86400, "expiresAt": "…" }

Listing and deleting

  • GET /api/storage/:id lists objects: prefix narrows to a path, delimiter=/ groups keys into folders, and limit (up to 1,000, default 100) with cursor pages through; nextCursor is set while more remain.
  • POST { key, method: "delete" } removes one object. DELETE /api/storage/:id empties and deletes the bucket.
  • Sizes and file counts are measured daily (or on ?measure=1) and shown as a dash until a full count exists.

Who can do what

  • Viewers and read-only keys can list and get download links.
  • Uploading and deleting need the developer role (a full key).
  • A key limited to the project works for all of it.

Limits and scaling

  • One upload is at most 5 GB (one signed PUT; multipart is not offered yet).
  • Keys are up to 1,024 characters.
  • Buckets per plan: 2 hobby, 20 pro, 500 enterprise. There is no storage-bytes quota yet.
  • The bytes scale without limit and never pass through Harakumo; each signed URL is one API call.

Reference

SDK
const { bucket } = await hk.storage.create(project.id, { name: 'uploads' });
const { url } = await hk.storage.presign(bucket.id, { key: 'file.png', method: 'put', contentType: 'image/png' });
const { url: download } = await hk.storage.presign(bucket.id, { key: 'file.png', method: 'get', expiresIn: 3600 });
const { objects, folders, nextCursor } = await hk.storage.objects(bucket.id);   // first page
await hk.storage.deleteObject(bucket.id, 'file.png');

Vector DB

Each collection is its own vector index. The default is 768 dimensions — what Harakumo's default embedding model (bge-base) produces, so text you send fits as-is. Choose 384 (bge-small) or 1,024 (bge-large) to match those models, or 1,536 for vectors you make elsewhere; larger is refused.

vector engine

Embed, upsert, query

SDK
const { collection } = await hk.vectors.create(project.id, {
  name: 'docs', dimensions: 768, metric: 'cosine',
  metadataIndexes: [{ propertyName: 'lang', indexType: 'string' }],
});

// Send text and let Harakumo embed it (metered on AI credits), or send your own "values".
await hk.vectors.upsert(collection.id, [
  { id: 'doc-1', text: 'Peaches are in season in July.', metadata: { lang: 'en' } },
  { id: 'doc-2', text: 'Mangoes ripen best at room temperature.', metadata: { lang: 'en' } },
]);

const { matches } = await hk.vectors.query(collection.id, {
  text: 'when can I buy peaches?', topK: 3, filter: { lang: 'en' }, returnMetadata: 'all',
});

Namespaces and filters

  • Give each vector a namespace (one per end user, say); a query that names the namespace sees only its vectors.
  • Filters work on metadata fields made filterable with metadataIndexes at create time, or later with POST /api/vectors/:id { action: "index-metadata", propertyName, indexType }. Only vectors written after a field is indexed are filterable. Filtering on a field that is not indexed is a 400 that says so.
  • Delete vectors with POST /api/vectors/:id { action: "delete", ids: [...] }.

Limits

  • Upsert up to 1,000 vectors per request (100 when sending text); metadata up to 10 KB per vector.
  • topK up to 100, or 50 when returning full metadata or values.
  • Upserts are applied in the background: allow a few seconds (up to about 30) before new vectors show in queries. GET /api/vectors/:id reports the index's own count.
  • Collections per plan: 1 hobby, 10 pro, 200 enterprise.
  • Keys: a key limited to the project works for everything here; a read-only key can query but not upsert or delete.

Reference

SDK
const { embeddings } = await hk.ai.embed(['fresh mango', 'ripe banana'], 'bge-base');   // 768 dims
await hk.vectors.upsert(collectionId, [{ id: 'a', values: embeddings[0], metadata: { title: 'Mango' } }]);
const { matches } = await hk.vectors.query(collectionId, { vector: embeddings[1], topK: 5 });

Memory

"kv" is a key-value store with TTL, to the second (carts, sessions, flags). "conversation" is an ordered transcript: append turns and read the last N back to build the next prompt. "semantic" embeds every value as you write it and recalls by meaning, with similarity scores.

One store serves every user of your app: pass a scope (a user or session id) on each call. Values, transcripts and searches in one scope never mix with another's.

edge key-value store

A multi-user chat

REST from your server
const base = 'https://harakumo.com/api/memory/' + storeId;
const headers = { authorization: 'Bearer ' + process.env.HARAKUMO_API_KEY, 'content-type': 'application/json' };

// Append this user's turn to their own transcript
await fetch(base + '/messages', { method: 'POST', headers, body: JSON.stringify({ role: 'user', content: text, scope: userId }) });

// Read their last 20 messages to build the next prompt
const { messages } = await (await fetch(base + '/messages?scope=' + encodeURIComponent(userId) + '&limit=20', { headers })).json();

// Forget one user on request
await fetch(base + '/messages?scope=' + encodeURIComponent(userId), { method: 'DELETE', headers });

Scopes

  • A scope is 1–64 characters: letters, digits and . _ - : @ + = (so a user id or an email address fits as-is).
  • Pass it in the query string, or in the body of a PUT or POST.
  • A semantic search only matches values written in the same scope; a search with no scope sees only unscoped values.
  • PATCH /api/memory/:id { action: "flush", scope } clears one scope; without scope it clears the store.

Limits

  • Values up to 1 MB; keys up to 512 characters.
  • A conversation keeps the 1,000 most recent messages per scope; one read returns up to 500.
  • Semantic values become searchable about 30 seconds after they are written — a search right after a write can legitimately find nothing, and the response says so.
  • Each plan caps items per store and bytes per workspace for every kind of store; a write past the cap gets a 402 naming it. Expired values are removed nightly.
  • Semantic writes and searches are metered on AI credits (embedding), and searches share the embeddings rate limit.
  • Each semantic write, delete and search is one call from the workspace's 600 per 5 minutes, shared with databases and vectors. Flushing a semantic scope removes up to 1,000 values per call and answers partial: true until it is done; run it again.
  • Keys: a key limited to the project works for everything here; a read-only key can read and search but not write.

Reference

SDK
const { store } = await hk.memory.create(project.id, { name: 'session', kind: 'kv' });
await hk.memory.set(store.id, 'cart', { items: 3 }, { ttlSeconds: 3600, scope: 'user-42' });

const { store: notes } = await hk.memory.create(project.id, { name: 'notes', kind: 'semantic' });
await hk.memory.set(notes.id, 'fact-1', 'Peaches cost $4 per pound.');
const { matches } = await hk.memory.search(notes.id, { query: 'what does fruit cost?', topK: 3 });
// Scoped reads, searches and transcripts: pass ?scope= over REST (below).
AI

AI Gateway

Call POST /api/ai/chat with a model and messages. Every answer reports the model that answered, the provider, tokens, latency, why it stopped and exactly what it cost. Usage is paid from the workspace's included AI credit first, then the Balance.

harakumo-1 (alias auto) is a router: it tries vendor models in order within a tier (fast, balanced or deep) until one answers, and is billed at the price of the model that answered — Claude Sonnet leads the balanced tier. Name a model to pin it; llama-3.3-70b runs on Harakumo's edge inference. Some models are off until an admin turns them on in the AI Gateway settings.

AI gateway

Chat

curl
curl https://harakumo.com/api/ai/chat \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "model": "harakumo-1", "system": "Answer in one sentence.",
        "messages": [{ "role": "user", "content": "What is an edge function?" }], "max_tokens": 300 }'
# → { "output", "modelUsed", "provider", "finishReason", "truncated", "tokensIn", "tokensOut", "cost", "latencyMs" }
Streaming (server-sent events)
const res = await fetch('https://harakumo.com/api/ai/chat', {
  method: 'POST',
  headers: { authorization: 'Bearer ' + process.env.HARAKUMO_API_KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ model: 'claude-sonnet-5', stream: true, messages: [{ role: 'user', content: 'Tell me a story.' }] }),
});
// Events: "meta" (the model answering), "delta" ({ text }), then "done"
// ({ finishReason, truncated, tokensIn, tokensOut, cost }) or "error".
for await (const chunk of res.body) process.stdout.write(new TextDecoder().decode(chunk));

OpenAI-compatible

Point any OpenAI client at https://harakumo.com/api/ai/v1 with a Harakumo key. The request and response shapes are the same (streaming included), model is any Harakumo model id, and each reply carries a harakumo field with the provider, cost and any fallback taken.

OpenAI SDK
import OpenAI from 'openai';

const ai = new OpenAI({ apiKey: process.env.HARAKUMO_API_KEY, baseURL: 'https://harakumo.com/api/ai/v1' });
const r = await ai.chat.completions.create({ model: 'harakumo-1', messages: [{ role: 'user', content: 'Hi' }] });
console.log(r.choices[0].message.content);

Request fields

FieldMeaning
modelA model id, or harakumo-1 / auto. GET /api/ai/chat lists the models this workspace can use and its limits.
messages or promptA chat history (system, user, assistant roles), or one prompt string.
systemA system prompt.
max_tokensDefault 1,024, up to 8,192 (2,048 on edge models).
temperature0–2 (Claude models are capped at 1).
tierFor harakumo-1: fast, balanced or deep.
streamtrue for server-sent events.
projectIdAttribute the spend to a project (or send an X-Harakumo-Project header).

An answer that stopped at max_tokens has truncated: true and a note on how to get the rest.

Embeddings

POST /api/ai/embeddings { input, model } returns vectors from bge-small (384 dimensions), bge-base (768, the default) or bge-large (1,024), up to 100 inputs per call. They pair with Vector DB collections of the same size.

Money, limits and logs

  • Every plan includes AI usage each month ($1 hobby, $10 pro, $100 enterprise; Hobby only on the personal workspace, and vendor models need the owner's verified email). Then the Balance pays, at each model's list price with no markup.
  • When included credit and Balance are both gone, calls return 402 ai_credits_exhausted; past the workspace's spending limit, 402 spend_limit_reached. Nothing is charged for a refused call.
  • Rate limit per workspace: 60 a minute on hobby, 600 a minute on pro, 6,000 a minute on enterprise; chat and embeddings have separate buckets. Over it: 429 ai_rate_limited with Retry-After.
  • Input up to 200,000 characters per request (413 over it).
  • GET /api/ai/requests lists recent calls (model, tokens, cost, speed — never the prompt or answer), kept 30 days, with this month's spend by model and project.
  • The AI Gateway and embedding endpoints are workspace-wide: use a workspace key, not a project key.

Reference

SDK
// One-shot prompt (the playground endpoint):
const answer = await hk.ai.generate({ model: 'harakumo-1', prompt: 'Summarize edge functions.' });
console.log(answer.output, answer.modelUsed, answer.cost);

// Embeddings for Vector DB (bge-small 384 · bge-base 768 · bge-large 1024):
const { embeddings, dimensions } = await hk.ai.embed(['fresh mango', 'ripe banana'], 'bge-base');
// Chat history, system prompts and streaming: POST /api/ai/chat or the OpenAI-compatible URL.

Agents

An agent calls tools in a loop until the task is done or it reaches its step limit. The tools are Harakumo's own actions (list projects, read usage, query vectors … and, when allowed, deploy, create or delete). They run with a temporary developer key made for the run and revoked afterwards.

Agents are for acting on your own Harakumo resources today. Custom HTTP tools, memory attached to an agent and background runs with a callback are not available yet.

agent runtime

Create and run

SDK
const { agent } = await hk.agents.create(project.id, {
  name: 'ops', model: 'claude-sonnet-5',
  instructions: 'Report failed deploys and projects close to their plan limits.',
  tools: ['list_projects', 'get_usage', 'list_deployments'],
  maxSteps: 8,
});
const { run } = await hk.agents.run(agent.id, 'What needs attention today?');
// run.status, run.output, run.steps, run.cost_micros, run.model, run.stop_reason

Settings

FieldMeaning
modelA tool-capable model (Claude, GPT, Grok, Kimi) or auto (runs on claude-sonnet-5).
toolsTool names from GET /api/agents/tools (add ?writes=1 to see the ones that change things).
allowWritesfalse by default: only read tools. true lets the agent use tools that change things — deploy, create, delete, send mail, spend the Balance on domains. Turn it on deliberately.
maxSteps1–24, default 8. A run that hits it stops as failed with stop_reason step_limit.
instructionsUp to 16,000 characters.

Limits and cost

  • Each step is charged as it happens at the model's price, from included AI credit and then the Balance; a run stops when the money or the spending limit runs out.
  • Runs: 6 a minute per workspace; 1 at once on hobby, 5 at once on pro, 20 at once on enterprise. Input up to 32,000 characters.
  • A run finishes inside the HTTP request; a run still marked running after 60 minutes is marked failed (stale).
  • Edit an agent with PATCH /api/agents/:id (model, instructions, tools, maxSteps, allowWrites); its run history is kept.

Reference

SDK
const { agent } = await hk.agents.create(project.id, { name: 'triage', model: 'auto', tools: ['list_projects'] });
const { run } = await hk.agents.run(agent.id, 'List my projects and which are live.');
const { agent: a, runs } = await hk.agents.get(agent.id);   // the 20 most recent runs with their steps

Media

Create a video asset, upload the file to its one-time upload URL, and play it over HLS when it is ready. Live channels get an RTMPS ingest URL and a stream key; point your encoder at it and go live.

media streaming

Details

  • Video only, up to 1 hour per upload. Kinds "image" and "audio" answer 501 — keep those files in Storage.
  • Status runs pending_upload → processing → ready. Size and duration appear once the file is processed.
  • An expired upload URL is replaced with a fresh one (POST /api/media/:id/upload); once a file has arrived, no new URL is issued.
  • Stream keys and upload URLs are visible to developers and above, never to viewers.
  • Live channels do not record.
  • Assets per plan: 5 hobby, 100 pro, 1,000 enterprise.

Reference

SDK
const { asset, uploadUrl } = await hk.media.create(project.id, { name: 'promo', kind: 'video' });
// POST multipart/form-data with field "file" to uploadUrl, then poll:
const { asset: ready } = await hk.media.get(asset.id);   // pending_upload → processing → ready
const live = await hk.media.create(project.id, { name: 'launch', kind: 'live' });   // ingest URL + stream key
App services

Auth pools

Each project can have one pool. End users sign up and log in, Harakumo stores them (passwords hashed with PBKDF2) and returns a signed token your app checks on each request.

Enabling a pool returns three values. The client secret (as_…) lets your server act for this pool only; it is shown once and can be rotated. The JWT secret (js_…) verifies tokens; developers can show it again. The client id (ap_…) is publishable and keys the browser endpoints.

auth pools

Two ways to call it

FromEndpointCredential
Your serverPOST /api/auth-pools/:poolId with an actionBearer client secret (as_…), or an API key
A browser or mobile appPOST /api/auth-pools/public/:clientId/:actionNone — the client id is in the URL. Off until you turn it on.

Enable a pool

curl
curl -X POST https://harakumo.com/api/projects/<projectId>/auth \
  -H "Authorization: Bearer $HARAKUMO_API_KEY"
# 201 → { "pool": {…}, "clientId": "ap_…", "clientSecret": "as_…", "jwtSecret": "js_…",
#         "serverEndpoint", "browserEndpoint", "verifyEndpoint", "algorithm": "HS256" }

Store clientSecret and jwtSecret as environment variables of the project (for example HK_CLIENT_SECRET and HK_JWT_SECRET), with the pool id, client id and issuer. The issuer is https://harakumo.com/auth/<project-slug>.

Sign-up and login from your server

Every call names its action: signup, login, send-verification, verify-email, forgot-password or reset-password. Your server keeps the returned token in its own cookie and verifies it on each request. The example uses express and jose (npm install express jose).

Express
import express from 'express';
import { jwtVerify } from 'jose';

const POOL = 'https://harakumo.com/api/auth-pools/' + process.env.HK_POOL_ID;
const secret = new TextEncoder().encode(process.env.HK_JWT_SECRET);

async function pool(body) {
  const r = await fetch(POOL, {
    method: 'POST',
    headers: { authorization: 'Bearer ' + process.env.HK_CLIENT_SECRET, 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
  const data = await r.json();
  if (!r.ok) throw Object.assign(new Error(data.error), { status: r.status });
  return data;   // { user, token, expiresIn }
}

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
  try {
    const { token } = await pool({ action: 'signup', email: req.body.email, password: req.body.password });
    res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 864e5 }).json({ ok: true });
  } catch (err) {
    res.status(err.status || 500).json({ error: err.message });   // 409 when the email already has an account
  }
});

app.post('/login', async (req, res) => {
  try {
    const { token } = await pool({ action: 'login', email: req.body.email, password: req.body.password });
    res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 864e5 }).json({ ok: true });
  } catch (err) {
    res.status(err.status === 429 ? 429 : 401).json({ error: err.message });
  }
});

// Verify locally on every request: no call to Harakumo.
async function requireUser(req, res, next) {
  const token = (req.headers.cookie || '').match(/(?:^|; )session=([^;]+)/)?.[1];
  try {
    const { payload } = await jwtVerify(token || '', secret, {
      algorithms: ['HS256'], issuer: process.env.HK_ISSUER, audience: process.env.HK_CLIENT_ID,
    });
    req.user = payload;   // sub, email, name, email_verified …
    next();
  } catch {
    res.status(401).json({ error: 'Sign in first' });
  }
}

app.get('/me', requireUser, (req, res) => res.json({ id: req.user.sub, email: req.user.email }));
app.listen(process.env.PORT || 3000);

Token claims

ClaimValue
issThe pool's issuer, https://harakumo.com/auth/<project-slug>
audThe pool's client id
subThe end user's id
email, nameAs signed up
email_verifiedtrue once they entered a verification code
poolThe pool id
tvToken version: moves on when the user is reset or signed out everywhere
iat, expIssued and expiry; tokens last 7 days (HS256)

Local verification cannot see a revocation until the token expires. POST /api/auth-pools/:id/verify { token } also refuses tokens of deleted users and users signed out everywhere (reason "revoked"); call it where that matters.

Sign-in from the browser or a mobile app

  1. Turn browser access on: PATCH /api/auth-pools/:id/settings { publicEnabled: true }. Letting anyone create an account is a separate switch, publicSignup: true.
  2. The project's own addresses and verified custom domains are allowed automatically. Add other origins with allowedOrigins (https only; http only for localhost; up to 20). A request from any other Origin is refused before anything runs.
  3. Call the public endpoints with the client id. GET …/session with Authorization: Bearer <token> answers who is signed in.
Browser
const AUTH = 'https://harakumo.com/api/auth-pools/public/ap_…';

const r = await fetch(AUTH + '/login', {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ email, password }),
});
const { token, user, error } = await r.json();

const me = await (await fetch(AUTH + '/session', { headers: { authorization: 'Bearer ' + token } })).json();

Browser limits: sign-up 10 per IP an hour and 600 per pool an hour; login 30 per IP per 10 minutes; codes 20 per IP an hour.

Email verification and password reset

  1. send-verification { email } emails a 6-digit code; verify-email { email, code } marks the address verified and returns a fresh token.
  2. forgot-password { email } emails a reset code; reset-password { email, code, password } sets the new password, signs the user out everywhere else and returns a token.
  • Codes last 15 minutes and allow 5 tries; at most 5 codes an hour per address.
  • They are sent under your app's sender name and count toward the workspace's email quota.
  • Browser answers never reveal whether an email has an account.

Managing users and secrets

  • GET /api/auth-pools/:id?q=&before=&limit= lists users (up to 200 a page).
  • PATCH /api/auth-pools/:id/users/:userId with action reset-password (with password), send-password-reset, revoke-sessions or unlock. DELETE removes the user; their tokens stop verifying.
  • GET /api/auth-pools/:id/secret shows the JWT secret again (developer and up, audited).
  • POST /api/auth-pools/:id/secret { which: "jwt" } rotates it, keeping the old one valid for 24 hours (or keepPrevious: false to revoke at once). { which: "client" } replaces the client secret immediately. Rotating needs an admin.

Limits and what is not available yet

  • Passwords 8–256 characters. 10 wrong passwords lock the account for 15 minutes (429 with Retry-After).
  • No social login, no end-user MFA, no hosted sign-in page, no refresh tokens and no RS256/JWKS yet.
  • Pools per plan: 1 hobby, 10 pro, 100 enterprise; end users per pool are not capped.
  • Verifying tokens locally scales with your app. Sign-ups and logins run on the shared platform and suit apps with thousands of active users.

Reference

SDK
// On your server, the client secret works as the SDK key — for this pool's sign-in actions only.
const pool = new Harakumo({ apiKey: process.env.HK_CLIENT_SECRET });
const { token } = await pool.auth.signup(poolId, { email: 'user@yourapp.com', password: 'correct-horse-9' });
const { token: t2 } = await pool.auth.login(poolId, { email: 'user@yourapp.com', password: 'correct-horse-9' });
const { valid, claims, reason } = await pool.auth.verifyToken(poolId, t2);

// Managing the pool needs an API key:
const { pool: p, clientId, clientSecret, jwtSecret } = await hk.auth.enable(project.id);

Payments

Harakumo is the merchant: the buyer pays on a hosted checkout, the money is recorded as your project's Earnings, and after the hold the workspace owner withdraws it to a payout account in their own name (a one-time identity check with our payout provider).

Earnings are not the workspace Balance. The Balance is money you load to pay Harakumo; Earnings are money your customers paid you. They are never mixed.

payments

The flow

  1. Enable payments on the project — one call, live at once.
  2. Create a checkout link for an amount, with your own reference and metadata and a successUrl on your site.
  3. Send the buyer to the link. It can be paid once and expires after 24 hours.
  4. After paying, the buyer returns to your successUrl with session_id added.
  5. Your server confirms the payment with the session lookup, or receives the payment.succeeded webhook.
  6. The net (amount less the fee) is added to Earnings, held 7 days, then withdrawable by the workspace owner.

Fees and rules

ItemRule
Platform fee3.9% + 30¢ per sale — the processor's cost plus 1% for Harakumo
Hold7 days per payment before it can be withdrawn
Amount$0.50 to $1,000,000, USD, one-time payments
Linksingle use, expires after 24 hours
Refundyour Earnings go down by the same share of your net as the refund is of the payment
Disputethe payment's net is held back from Earnings while the dispute is open; a won dispute gives it back
Live linksneed a verified workspace-owner email (403 email_unverified otherwise)

Create a checkout link

FieldRules
amountCentsrequired, at least 50
description, customerEmailoptional
referenceyour own id for the sale (order, user …), up to 200 characters; filter lists by it
metadataup to 20 string keys; keys starting with hk_ are reserved
successUrlhttp or https. session_id={CHECKOUT_SESSION_ID} is added unless you put the placeholder in yourself. Without it the buyer sees Harakumo's public thank-you page.
cancelUrlwhere the buyer goes if they back out
curl
curl -X POST https://harakumo.com/api/payments/<accountId> \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "amountCents": 2900, "description": "Pro plan", "customerEmail": "buyer@example.org",
        "reference": "order-1042", "metadata": { "userId": "42" },
        "successUrl": "https://yourapp.com/thanks", "cancelUrl": "https://yourapp.com/cart" }'
# 201 → { "url": "…", "session": { "id", "state": "open", "expires_at", "single_use": true, "reference", "metadata", … }, "feeCents" }
SDK
const { url, session } = await hk.payments.createSession(accountId, {
  amountCents: 2900, description: 'Pro plan', reference: 'order-1042', metadata: { userId: '42' },
  successUrl: 'https://yourapp.com/thanks', cancelUrl: 'https://yourapp.com/cart',
});

Confirm a payment

On your success page, look the session up with the id from the redirect. paid is true once the processor has confirmed it (an open checkout is checked with the processor on read, so this never waits on a webhook).

fetch
const r = await fetch('https://harakumo.com/api/payments/' + accountId + '/sessions/' + encodeURIComponent(sessionId), {
  headers: { authorization: 'Bearer ' + process.env.HARAKUMO_API_KEY },
});
const { paid, payment, session } = await r.json();
// payment: { amount_cents, fee_cents, net_cents, customer_email, reference, metadata, status, available_at, … }

Webhooks

Register an https URL with PATCH /api/payments/:accountId { action: "set-webhook", webhookUrl }. The signing secret (hkwhsec_…) is returned once. Events: payment.succeeded, payment.refunded, payment.disputed, payment.dispute_won, and ping (from action: "test-webhook").

Each delivery is a POST with the JSON event and three headers: Harakumo-Event (the type), Harakumo-Delivery (the event id — use it to ignore duplicates) and Harakumo-Signature (t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">). Answer 2xx within 5 seconds; redirects are not followed. Failed deliveries are retried up to 8 times, backing off 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours.

Verify and handle (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// Verify against the raw body, before any JSON parsing.
app.post('/webhooks/harakumo', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = Object.fromEntries((req.get('harakumo-signature') || '').split(',').map(p => p.trim().split('=')));
  const t = Number(parts.t);
  const expected = crypto.createHmac('sha256', process.env.HARAKUMO_WEBHOOK_SECRET)
    .update(t + '.' + req.body.toString('utf8'))
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - t) <= 300;
  const valid = fresh && typeof parts.v1 === 'string' && parts.v1.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!valid) return res.status(400).send('bad signature');

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.type === 'payment.succeeded') {
    const p = event.data.payment;   // mark order p.reference as paid — idempotently
  }
  res.sendStatus(200);
});
Event body
{
  "id": "evt_ps_318", "type": "payment.succeeded", "created": 1790000000,
  "data": { "payment": {
    "id": 318, "account_id": 9, "project_id": 12, "session_id": 771, "session_key": "…",
    "reference": "order-1042", "metadata": { "userId": "42" },
    "amount_cents": 2900, "fee_cents": 143, "net_cents": 2757, "refunded_cents": 0,
    "currency": "usd", "customer_email": "buyer@example.org", "country": "US",
    "status": "succeeded", "created_at": "2026-09-25 14:02:11"
  } }
}

Rotate the secret with action: "rotate-webhook-secret". Every delivery and its result is listed on the project's Payments page.

Lists

GET /api/payments/:accountId/payments and /sessions page through one project's payments and checkout links (?limit=1..100&cursor=&status=&reference=, answering nextCursor). GET /api/payments lists the workspace's recent payments across projects. GET /api/projects/:id/payments returns the account, recent activity and earnings (balance, held, available, withdrawn, pending, next release).

Refunds, withdrawals and turning it off

  • Refund all or part of a payment: PATCH { action: "refund", paymentId, amountCents? }.
  • Withdraw: connect a payout account once (withdraw-setup returns an identity-check link), then withdraw sends what is available. A withdrawal the processor has not confirmed stays pending until it does; it is never re-credited on a guess.
  • Refunds, payout setup, the payout dashboard, withdrawals and permanent delete need the workspace owner signed in to the dashboard. API keys, developers and AI connectors get 403 payouts_require_owner.
  • DELETE /api/payments/:accountId disables payments: no new links, but earnings, history and the payout account are kept and links already sent are still credited. POST /api/projects/:id/payments turns it back on. ?permanent=true erases the account, only when nothing is owed or open.

Limits and scale

  • USD and one-time payments only: no subscriptions, products, coupons or tax yet. Each link is for one payment.
  • Payment accounts per plan: 1 hobby, 10 pro, 100 enterprise (one per project).
  • Payments are recorded in Harakumo's ledger, not polled by your app: use the webhook for volume, and the session lookup on the success page.
  • Keys: a key limited to the project (full access) can enable payments, create links, look sessions up, list payments and set the webhook. A read-only key can look up and list. Money leaving — refunds and withdrawals — always needs the owner.

Reference

SDK
const { account } = await hk.payments.enable(project.id);
const { url } = await hk.payments.createSession(account.id, { amountCents: 2900, description: 'Pro plan', successUrl: 'https://yourapp.com/thanks' });
const { account: a, earnings } = await hk.payments.get(project.id);
const { totals, funnel, daily } = await hk.payments.analytics(project.id);
// Session lookup, lists, refunds and webhooks: REST (below).

Mail

Every email carries your app's name and comes from, in order: the from address on the email, the project's From address, or no-reply@harakumo.com. Recipients see your project as the sender — "Vertex <hello@vertex.app>" or "Vertex <no-reply@harakumo.com>", never Harakumo.

The name comes from, in order: fromName on the email, the project's saved sender name, the project's name, then a team workspace's name. A workspace with several projects must pass projectId.

mail

Send

curl
curl -X POST https://harakumo.com/api/mail/send \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "to": "user@yourapp.com", "subject": "Welcome", "text": "Hello!", "projectId": 12 }'
# 200 → { "message": { "status": "sent", "from_name": "Vertex", "from_email": "no-reply@harakumo.com", "message_id": "…" },
#         "from_fallback": null }

# From your own verified domain, with a Reply-To (both optional)
curl -X POST https://harakumo.com/api/mail/send \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "to": "user@yourapp.com", "subject": "Welcome", "text": "Hello!", "projectId": 12,
        "from": "hello@yourdomain.com", "replyTo": "support@yourdomain.com" }'

Mail to reserved test domains (example.com, .test, .invalid, .localhost) is never delivered: it is logged with status simulated. Test with a real address.

What the statuses mean

StatusMeaning
sentThe provider accepted the message. Delivery, bounces and opens are not reported yet.
simulatedNot sent: a reserved test address, or no provider in this environment.
failedThe provider refused it; the response is 502 with the reason. Failed sends do not count toward the quota.

Sender names

  • Set one per project on the Mail page (Sender names & addresses) or with PATCH /api/projects/:id { mailSenderName } ("" resets it), or per email with fromName.
  • Up to 64 characters. Names that look like Harakumo, or are only generic words ("Support", "No Reply"), are refused, and subjects may not mention Harakumo.

Send from your own domain

Add a domain you own once, and your apps can send from any address on it, like hello@yourdomain.com. How much you do depends on where the domain's DNS lives. Harakumo finds out for you when you add it.

A subdomain like mail.yourdomain.com works too. Sending domains per workspace: 1 on hobby, 10 on pro, 50 on enterprise.

You haveWhat happens
A domain bought on HarakumoNothing to copy. Harakumo adds every email record for you. If its DNS hosting is off, turn it on first (Domains page, or host_domain_dns from an AI assistant).
A domain connected to a project, nameservers already switchedNothing to copy. Harakumo adds every email record for you and it is verified within minutes.
A domain connected to a project, nameservers not switched yetSwitch the nameservers and sending turns on by itself. Or add the records yourself at your current provider instead.
A domain at another company (GoDaddy, Namecheap, Squarespace, Hostinger …)You add 3 required records (and 3 recommended ones) at that company. About 5 minutes of copying; Harakumo shows exactly what to copy and checks each record for you.

Until a domain is verified, your mail still goes out from no-reply@harakumo.com under your app's name, so nothing breaks while you set it up. Mail is never sent from an unverified domain.

If the domain's DNS is on Harakumo

  1. Open Mail → Sending domains and press Add a domain. Type the domain (for example yourdomain.com) and press Continue.
  2. Harakumo sees the DNS is here and says "Good news … nothing to copy". Press Set it up.
  3. Harakumo adds the email records to your DNS and confirms them. This usually takes a few minutes; the page updates by itself and the workspace owner gets an email.
  4. When it shows Verified, choose an address like hello@yourdomain.com for a project under Sender names & addresses.

If the card says Waiting for nameservers, the domain is set up on Harakumo but its nameservers still point at your old provider. Replace them at the company where you bought the domain with the two shown on the Domains page. Sending turns on by itself once they switch (usually within an hour, sometimes up to a day).

If the domain's DNS is at another company

  1. Open Mail → Sending domains and press Add a domain. Type the domain and press Continue, then choose Add the records yourself.
  2. Sign in to the company where you manage DNS for the domain (usually where you bought it) and open its DNS settings. The card tells you who that probably is ("Looks like GoDaddy manages DNS for yourdomain.com").
  3. Add the 3 required records. Copy each Type, Host and Value exactly; every cell has a copy button. The 3 recommended records help your mail reach inboxes, so add them too if you can.
  4. Save, come back and press Check now. The card says how far along you are ("2 of 3 required records found") and marks each record Found, Missing or Wrong.
  5. Most providers take a few minutes; some take up to a few hours. After you press Check now the page re-checks every minute for 10 minutes, and Harakumo keeps checking every 10 minutes in the background.
  6. When all required records show Found, the domain is verified (the owner gets an email). Choose an address on it for a project, or pass from when you send.

Add the records within 3 days of starting. After that the setup stops and Start again gives you fresh records. Rather not edit DNS? A domain bought on Harakumo can have its DNS hosted here in one step, and a domain you are already connecting to a project only needs its nameservers switched.

The records, in plain words

The Host is the part before your domain: <token>._domainkey means <token>._domainkey.yourdomain.com. For a subdomain like mail.yourdomain.com, each Host ends in .mail. The exact values are unique to your domain and are shown in the dashboard, the API response and the CLI.

  • The SPF and MX records go on bounces.yourdomain.com, so they never clash with the email you already receive at yourdomain.com or with its own SPF record.
  • Already have a DMARC record at _dmarc? Keep yours and skip ours: two DMARC records switch DMARC off. Any DMARC record counts as Found.
  • When Harakumo hosts the DNS it writes these records itself and never changes or removes a record you made.
TypeHostValueWhat it doesNeeded?
CNAME (3 of them)<token>._domainkeyshown in the dashboardProves the mail really comes from you (DKIM).Required
MXbouncesshown in the dashboard, Priority 10Handles bounced mail.Recommended
TXTbouncesshown in the dashboard (starts v=spf1)Lets Harakumo send for your domain (SPF).Recommended
TXT_dmarcv=DMARC1; p=none;Tells inboxes what to do with fakes (DMARC).Recommended

Where to add them, provider by provider

ProviderWhat to do
GoDaddySign in, open My Products, find your domain and choose DNS. Choose Add New Record and pick the Type. In Name, paste the Host: GoDaddy adds your domain for you, so leave it off. Paste the Value (for CNAME it is called Points to), keep TTL as is, and Save. For the MX record, use Priority 10.
NamecheapSign in, open Domain List and choose Manage next to your domain. Open Advanced DNS and choose Add New Record. Pick the Type, paste the Host, and paste the Value (for CNAME it is called Target). Save each record with the check mark. The MX record is optional. Skip it if your email uses Namecheap Email Forwarding or Private Email, because changing Mail Settings turns those off. Otherwise, to add it, set Mail Settings to Custom MX and add it there with Priority 10.
Squarespace (domains that were at Google Domains are here now)Open Domains in your Squarespace account and choose your domain. Go to DNS, then DNS Settings, and scroll to Custom records. Choose Add record, pick the Type, paste the Host and the Value (called Data), and Save. For the MX record, use Priority 10.
HostingerOpen Domains in hPanel, choose your domain, then DNS / Nameservers. Under Manage DNS records, pick the Type. In Name, paste the Host. In Points to (or Target, or TXT value), paste the Value. Keep TTL as is and choose Add Record. For MX, use Priority 10.
Any other providerFind the page called DNS, DNS zone or Advanced DNS. Add one record for each row: same Type, Host and Value. If it asks for the full name, turn on Show full names and use that instead of Host. A dot added at the end of a value is fine. If the record has a proxy or CDN switch, turn it off (DNS only). Stuck? Send the records to your provider's support and ask them to add them.

The dashboard picks your provider for you from the domain's nameservers; harakumo mail domains show yourdomain.com --provider godaddy prints the same steps (namecheap, squarespace, hostinger, other).

Checking, and what each status means

Check now (or POST /api/mail/domains/:id/check, or the check_mail_domain tool) looks each record up in public DNS right away and answers with every record's status. Checks less than 10 seconds apart return the last result. In the background, domains still being set up are checked every 10 minutes and verified ones every 6 hours.

  • Each record is found, missing or wrong. A wrong record also lists what public DNS shows instead (seen), so you can compare it with the Value.
  • records_summary.text is the one-line progress, like "2 of 3 required records found".
  • next_action is one of add_records, fix_records, wait, point_nameservers, wait_for_platform, restart or none, and next says it in a sentence.
Statuscan_sendMeaning
pendingfalseBeing set up: records are missing or being confirmed. next_action says what, if anything, you need to do.
verifiedtrue, or false while reason_code is setReady. Your apps can send from any address on the domain. A verified domain with a reason_code (for example provider_sandbox) is waiting on Harakumo, not on you.
failedfalseStopped. reason says why; Start again (or add it again) gets fresh records.
removedfalseYou removed it. It no longer counts toward your plan.

Record not found?

  • Give it time. New records usually show within minutes, but some providers take a few hours. Press Check now again later; nothing is lost while you wait.
  • The domain appears twice. Many providers add your domain to the Host automatically, so <token>._domainkey.yourdomain.com becomes <token>._domainkey.yourdomain.com.yourdomain.com. The card says "Your DNS provider added yourdomain.com a second time". Use only the Host shown.
  • Marked Wrong. The record exists but its value differs: compare the "We see" line with the Value column. Extra quotes or a dot at the end are fine; a missing character or a value cut short is not.
  • Proxy or CDN switch. If your provider has one on the record (sometimes called Proxied), turn it off so the record is DNS only.
  • Added at the wrong company. DNS is managed wherever the domain's nameservers point, which is not always where you bought it. The card's "Looks like … manages DNS" tells you where to go.
  • Setup stopped after 3 days, or the domain says it expired. Press Start again, replace the old records with the fresh ones shown, and check again.
  • Still stuck? Send the records to your DNS provider's support and ask them to add them exactly as shown.

When a domain can't send

A send asking for an address on a domain that cannot send yet still goes out, from no-reply@harakumo.com under your app's name. The response's from_fallback says why: { requested, reason_code, reason }. It is null when the address you asked for was used.

An explicit from on a domain this workspace never added is refused (400 from_domain_not_added) rather than sent from the shared address.

reason_codeWhat it meansWhat to do
records_missingSome required records are not visible yet.Add them at your DNS provider, then press Check now.
records_wrongA record is there but its value does not match, or its Host has the domain added twice (some providers add it for you).Fix it to match the Host and Value exactly, then press Check now.
zone_pendingThe domain is set up on Harakumo but its nameservers do not point here yet.Switch the nameservers (a domain bought on Harakumo with DNS hosting on is switched for you). Sending turns on by itself.
provider_pendingEverything is in place; the final confirmation is running.Nothing. It usually takes a few minutes.
edge_not_permittedHarakumo is finishing setup on its side for domains hosted here.Nothing. The page updates by itself and this never expires.
provider_sandboxThe domain is verified; sending from it opens once Harakumo's platform approval completes.Nothing.
expiredThe records were not found within 14 days.Start again for fresh records.
verification_failedThe records were not found within 3 days, so the setup stopped.Start again, add the fresh records, and check again.
verification_lostIt was verified, but its records are gone.Put the records back, then press Check now.
domain_left_workspaceThe domain's DNS is no longer hosted on Harakumo by this workspace.Add it again to set it up at its new DNS provider.
replacedAnother workspace started setting up the domain before its records were added here.If it is yours, press Start again, then add the fresh records shown.
domain_claimed_elsewhereAnother workspace on Harakumo now hosts or holds this domain. Controlling its nameservers wins over records.If it is yours, contact support@harakumo.com.
domain_removedThe project's saved From address is on a domain this workspace no longer has (only in from_fallback).Pick a new From address, or add the domain again.

Choosing the From address and Reply-To

  • Per project: on the Mail page under Sender names & addresses, or PATCH /api/projects/:id { mailFrom, mailReplyTo } ("" resets either). A domain that is still pending can be chosen in advance; mail uses no-reply@harakumo.com until it is verified.
  • Per email: from and replyTo on /api/mail/send (the SDK, CLI and send_email tool take the same).
  • The part before @ uses lowercase letters, numbers and . _ + - (up to 64 characters). postmaster@, abuse@, hostmaster@ and mailer-daemon@ are reserved for mail servers.
  • On the shared domain only no-reply@harakumo.com can be used. Add your own domain for any other address.
  • Reply-To is one address and needs no verification, but it cannot be a Harakumo address.
  • A domain belongs to one workspace. Once a workspace has verified yourdomain.com, other workspaces cannot send from it or from names under it. A setup another workspace started but never added records for does not block you: adding it takes over.
  • Removing a domain (Remove on its card, or DELETE /api/mail/domains/:id) moves projects that used it back to no-reply@harakumo.com; their sender names stay. Records Harakumo added are removed; records you added at another provider can then be deleted there.

Set it up with an AI assistant

With the connector attached, ask: "Set up yourdomain.com as a sending domain on Harakumo and walk me through it." The assistant adds the domain, shows you the steps and the exact records, checks them while you add them, and sets your project's From address once it is verified.

  • add_mail_domain — start (or start again). Returns the method, the records, the numbered steps and provider hints.
  • check_mail_domain — check DNS now; returns each record's status and "2 of 3 required records found".
  • list_mail_domains / get_mail_domain — status, records and every project's current From address.
  • set_mail_from — a project's From address and Reply-To.
  • remove_mail_domain — stop sending from a domain.
  • host_domain_dns — for a domain bought on Harakumo, host its DNS here so every record is added for you.
  • send_email — accepts from and replyTo.

From code: API, SDK and CLI

curl
# 1. Add the domain
curl -X POST https://harakumo.com/api/mail/domains \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "domain": "yourdomain.com" }'
# 201 → { "created": true, "domain": { "id": 7, "domain": "yourdomain.com", "method": "external", "status": "pending",
#   "can_send": false, "records_summary": { "text": "0 of 3 required records found", … },
#   "records": [{ "type": "CNAME", "host": "<token>._domainkey", "value": "<value shown>", "required": true, "status": "missing" }, …],
#   "steps": ["Sign in to the company where you manage DNS for yourdomain.com …", …], "next_action": "add_records", … } }

# 2. After adding the records at your DNS provider
curl -X POST https://harakumo.com/api/mail/domains/yourdomain.com/check -H "Authorization: Bearer $HARAKUMO_API_KEY"

# 3. Once verified, make it the project's From address
curl -X PATCH https://harakumo.com/api/projects/12 \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "mailFrom": "hello@yourdomain.com", "mailReplyTo": "support@yourdomain.com" }'
SDK
const { domain } = await hk.mail.domains.add('yourdomain.com');   // domain.records, domain.steps
// … add the records at your DNS provider …
await hk.mail.domains.check('yourdomain.com');                    // records_summary.text, status, can_send
await hk.mail.domains.waitUntilVerified('yourdomain.com');        // polls every 30 s, up to 10 minutes
await hk.mail.setFrom(project.id, { from: 'hello@yourdomain.com', replyTo: 'support@yourdomain.com' });
CLI
harakumo mail domains add yourdomain.com                  # steps and the records to copy
harakumo mail domains show yourdomain.com --provider godaddy
harakumo mail domains check yourdomain.com --wait
harakumo mail from 12 hello@yourdomain.com --reply-to support@yourdomain.com

Quotas and controls

  • 100 on hobby, 5,000 on pro, 100,000 on enterprise, counted over a rolling 30 days (402 plan_limit). The owner is emailed at 80% and 100%.
  • Custom sending domains: 1 on hobby, 10 on pro, 50 on enterprise (402 plan_limit). Pending and failed domains count until you remove them.
  • Your own controls at /api/mail/settings (admins): paused stops every send (403 mail_paused), dailyCap limits any 24 hours (429 daily_cap).
  • A suppression list at /api/mail/suppressions: a suppressed address is refused with 422 suppressed before anything is sent — record unsubscribes there.
  • One recipient per call, text and/or HTML, with an optional Reply-To. No attachments, cc, templates or batch sending yet.
  • Mail is workspace-wide: use a workspace key, not a project key.

Reference

SDK
await hk.mail.send({ to: 'user@yourapp.com', subject: 'Welcome', text: 'Hello!', projectId: project.id });
await hk.projects.update(project.id, { mailSenderName: 'Vertex' });                       // set once
await hk.mail.send({ to, subject, html, projectId: project.id, fromName: 'Vertex Billing' }); // or per email
const { messages } = await hk.mail.messages();

// Your own domain
await hk.mail.domains.add('yourdomain.com');
await hk.mail.domains.check('yourdomain.com');
await hk.mail.setFrom(project.id, { from: 'hello@yourdomain.com', replyTo: 'support@yourdomain.com' });
const { from_fallback } = await hk.mail.send({ to, subject, text, projectId: project.id, from: 'hello@yourdomain.com', replyTo: 'support@yourdomain.com' });
Web

Domains

Every domain in the workspace is one entry, keyed by its root (GET /api/domains), with one plain-word state and the next step. www is served together with the apex.

domains

Buy, connect or transfer?

You want toDo this
A new nameBuy it here. It is paid from the Balance or covered by your plan's included domains, and can connect to a project in the same call.
Use a name you own elsewhereConnect it: add it to a project and switch its nameservers to the two Harakumo gives you.
Move a name you own to HarakumoTransfer it in with its auth code; the price includes a year of renewal.

Buying

  1. Search: GET /api/domains/search?q=myshop (or myshop.org to search that name first). Each result has available (null means it could not be checked — not taken), sellable, price_cents and renewal_price_cents.
  2. Buy: POST /api/domains/registrations { domain, projectId? }. The live price is checked again; if it is above the price you saw (expectedPriceCents) nothing happens.
  3. With a projectId the domain connects automatically and goes live within minutes to hours as DNS settles; the owner gets an email when it is live.
  • Endings sold today: .com, .dev, .app, .io, .ai, .cloud. Automatic purchases above $100 a year are refused.
  • Included domains: Pro includes 1 standard domain a year and Enterprise 5 (a standard domain costs $15 or less). Extra domains, premium names and every domain on Hobby are paid from the Balance. There is no cap on how many you buy — each is paid for.
  • A Balance too small answers 402 insufficient_balance with the shortfall and where to add funds; nothing is charged. The card on file is charged only when a signed-in owner asks (payWithCard: true).
  • Purchases count toward the workspace's monthly spending limit, if one is set.
  • Harakumo registers the domain through its registrar account and manages it for your workspace. Registries lock a domain for 60 days after registration or transfer.

Connecting a domain you own

  1. Add it to a project: POST /api/projects/:id/domains { hostname, connect: true }. Harakumo checks no other workspace owns that root domain.
  2. Harakumo copies the records your domain answers with today (email included) into its DNS, and returns two nameservers. Review the copied records in DNS before switching.
  3. Set those nameservers at your registrar. The domain goes live on its own within about 10 minutes of the change being visible — there is nothing to press.

Switching nameservers moves all DNS for the root domain, even when you only connect a subdomain. Connecting by CNAME is not available yet.

DNS records

Any domain whose DNS Harakumo hosts — bought, transferred in or connected by nameservers — has editable records at /api/domains/zones/{root}/dns (GET, POST, PATCH, DELETE). Record types: A, AAAA, CNAME, MX, TXT and NS. SRV and CAA are not supported yet.

Add an MX record
curl -X POST https://harakumo.com/api/domains/zones/myshop.com/dns \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "type": "MX", "name": "@", "content": "mail.example.net", "priority": 10 }'

Want email from this domain? Mail → Sending domains sets it up (automatically when its DNS is here).

Renewal

  • Bought domains auto-renew by default (autoRenew: false at purchase to opt out). Renewal happens in the last 7 days before expiry, at renewal_price_cents, paid from the Balance. Warnings go out 30, 7 and 1 days before.
  • The card covers a renewal only if the owner, signed in, turned on card backup (autoRenewCard: true at purchase, or PATCH { action: "auto-renew", enabled: true, cardBackup: true }). Renewals set up by developers, API keys or transfers pay from the Balance only.
  • A domain past expiry is marked expired; renewing it within 30 days points it back at its DNS.

Stop serving, DNS only, other nameservers

  • DELETE /api/domains/registrations/:id (or PATCH { action: "detach" }) stops serving the project. DNS, email and renewal stay.
  • PATCH { action: "host-dns" } hosts DNS here without a project; { action: "set-nameservers", nameservers: [...] } points the domain at another provider; { action: "connect", projectId } serves another project.
  • Deleting a project detaches its domains; bought domains stay in the workspace and keep renewing.

Transfers

  • In: unlock the domain at your current registrar, get its auth code, and POST /api/domains/transfers { domain, authCode, dryRun: true } to validate and quote without paying. Then send it again without dryRun (with expectedPriceCents). The Balance pays once; the transfer completes in 5–7 days.
  • Out: PATCH /api/domains/registrations/:id { action: "transfer-out" }. The auth code arrives within five calendar days (usually sooner), by email and in the dashboard. The domain keeps working until it has moved.

Limits

  • Custom domains connected per plan: 2 hobby, 20 pro, 500 enterprise.
  • Up to 10 new DNS zones per workspace a day.
  • Domain routes are workspace-wide: use a workspace key. Adding a hostname to a project (/api/projects/:id/domains) accepts that project's key.

Reference

SDK
const { results } = await hk.domains.search('myshop');     // [{ domain, available, sellable, price_cents, renewal_price_cents }]
const pick = results.find(r => r.available && r.sellable);
const { registration, paidFrom } = await hk.domains.register(pick.domain, { projectId: project.id, expectedPriceCents: pick.price_cents });
// 402 insufficient_balance → err.body.shortfall_usd, err.body.add_funds_url

await hk.domains.add(project.id, { hostname: 'app.myshop.dev' });   // a domain you own; connect: see REST
const check = await hk.domains.transferIn('myshop.com', { authCode: 'EPP-CODE', dryRun: true });

CDN & Shield

Both settings apply to every static site in the workspace and take effect on each project's next deploy. Volumetric attack protection runs at the network layer for every site and needs no setting.

Settings

SettingMeaning
CDN cacheTtlSecondsHow long browsers and the edge keep a page (default 3,600 seconds). Turning the CDN off stops caching: every visit is served fresh.
Shield blockedCountriesTwo-letter country codes whose visitors are refused.

Server apps and functions are not affected by these settings yet. Changing them needs an admin.

Reference

REST
GET /api/services/cdn · PUT /api/services/cdn        { "enabled": true, "cacheTtlSeconds": 3600 }
GET /api/services/shield · PUT /api/services/shield  { "enabled": true, "blockedCountries": ["XX"] }
Account & billing

Workspaces, roles & teams

A workspace (called an organization in some API paths) is the unit of billing and access: it has one plan, one Balance, one team and its own API keys, audit log and settings. Signing up creates your personal workspace; create more from the workspace switcher. How many you can own depends on the best plan among them.

Roles

RoleCan
OwnerEverything, including the plan, cards, the Balance, spending limits, withdrawals and refunds, and deleting the workspace.
AdminMembers and invitations, API keys, workspace settings (CDN, Shield, AI Gateway models, mail controls), the audit log, reading billing, deleting projects.
DeveloperCreate, change and delete resources and deployments; read environment variables, function source and stream keys.
ViewerRead only — no environment variables, secrets or function source.

Team members

  • Admins invite by email. The invitation is emailed, and accepting it needs a verified email at the invited address.
  • Seats per plan: 3 hobby, 10 pro, 100 enterprise. Pending invitations hold a seat; a full workspace answers 402 plan_limit, and accepting re-checks.
  • Removing a member lists the API keys they created so you can revoke them.

What lives where

  • In the workspace: the plan, Balance and cards, members, API keys, the audit log, domains bought, repos, the mail log and settings, and CDN, Shield and AI Gateway settings.
  • In a project: its site or app, environment variables, custom domains, and its databases, buckets, functions, vector collections, memory stores, agents, auth pool, payment account and media.

Deleting a workspace or account

  • Deleting a workspace cancels its subscription immediately (no proration refund) and tears down every project.
  • It is refused while the workspace owns unexpired bought domains (transfer them out, or turn off auto-renew and let them lapse), has a domain transfer, refund or Balance top-up in progress, or holds project earnings.
  • A positive Balance is given up, not refunded, and only when you say so: send { "confirmForfeit": true } (the first answer is 409 confirm_forfeit_required).
  • Deleting your account (DELETE /api/auth/me { password, confirmForfeit? }) applies the same rules to every workspace it removes.

Reference

REST
GET    /api/orgs                        # your workspaces
POST   /api/orgs                        { "name" }                              # browser session
GET    /api/orgs/:id/members · PATCH · DELETE                                    # admin to change
POST   /api/orgs/:id/invitations        { "email", "role": "admin"|"developer"|"viewer" }
DELETE /api/orgs/:id                    { "confirmForfeit"? }                   # owner

Security & data isolation

Every request is checked the same way: who is calling, which workspace, what their role allows, and whether the resource belongs to that workspace's project. An id from another workspace answers 404 before any work happens.

Your account

  • Two-factor sign-in: Settings → Account. Scan the code with an authenticator app, confirm a code, and keep the recovery codes. Sign-in then asks for a code (the API answers { mfaRequired, ticket }; complete with POST /api/auth/login/mfa { ticket, code | recoveryCode }). Turning it off needs your password and a code.
  • Changing your email needs your current password (and a code when two-factor is on), then a code sent to the new address. The old address is told.
  • A password reset or change signs out your other sessions and disconnects AI connectors.
  • Sessions last 30 days; Settings lists them and signs the others out.
  • Changes made with the dashboard cookie are refused when they come from another site.

Keys and connectors

  • API keys can be read-only, limited to one project, or both (see API keys & authentication). Give each app the narrowest key that works.
  • AI connectors are connected read-only or read-and-manage; a read-only connector cannot change anything. Changes still ask you first in the assistant.
  • No key or connector can change your email, password or sessions, accept invitations, or move money out — those need you in a browser.
  • Revoke keys in Settings → API access and connectors in Dashboard → Connectors; both take effect immediately.

How your data is kept apart

LayerSeparation
Every API callYour credential resolves to one workspace; resources are found through project → workspace, never by id alone.
DatabasesEach database is its own SQLite database. SQL in one can never reach another.
StorageEach bucket is its own bucket in object storage; a signed URL is for one key in one bucket.
Vector DBOne index per collection.
MemoryRows in Harakumo's control database, keyed by store and scope; searches never cross scopes.
Functions and static sitesEach runs as its own isolated edge program with only its project's environment variables and no access to platform storage.
Server appsEach deployment runs in its own container.
Auth poolsOne per project, with its own signing secret.
PaymentsBuyers pay into Harakumo's processor account; each project's share is tracked in a ledger, and withdrawals go only to the payout account the workspace owner connected.
MailEach workspace sends from no-reply@harakumo.com or from its own verified domains (a domain belongs to one workspace), with its own log and suppression list.
DomainsEach root domain has one owning workspace; no other workspace can add a hostname under it.

How secrets are stored

  • Passwords (yours and your end users'): PBKDF2 hashes.
  • Sessions, API keys, connector tokens and auth-pool client secrets: SHA-256 hashes; the plaintext is shown once.
  • Two-factor recovery codes: hashed, and each works once.
  • Environment variables and auth-pool JWT secrets: stored readable, because they are injected into deploys and used to sign tokens. Encryption at rest is planned.

Reference

REST
POST /api/auth/login           { "email", "password" }        → session, or { mfaRequired, ticket }
POST /api/auth/login/mfa       { "ticket", "code" | "recoveryCode" }
GET|POST|PUT|PATCH|DELETE /api/auth/mfa   # status, set up, confirm, new recovery codes, turn off (browser session)
GET  /api/auth/sessions · DELETE                                # sign out other sessions
GET  /api/security/overview    # the workspace's security checks

Billing, Balance & AI credits

Each workspace has one plan and one Balance. The plan decides what you can create and what is included each month; the Balance pays for anything beyond. Money your customers pay you through Payments is Earnings, held per project — it never pays Harakumo and the Balance never pays you.

Plans

Limits & scaling lists every count. Plans limit how many things you create; data size, storage bytes and bandwidth have no plan quota yet.

HobbyProEnterprise
Price (per workspace)Free$20 a month$500 a month
AI usage included$1 a month (personal workspace only)$10 a month$100 a month
Domains includedNone1 a year (up to $15 each)5 a year (up to $15 each)
Emails (rolling 30 days)1005,000100,000

Changing plan

  • Only the owner changes the plan, from Dashboard → Billing or POST /api/billing { plan }.
  • Upgrading charges the prorated difference now; the plan changes once that payment succeeds.
  • Downgrading between paid plans starts at the next renewal. Moving to Hobby ends the subscription at the end of the period; POST /api/billing { action: "resume" } (Keep plan) withdraws a pending cancel or downgrade.
  • Nothing is deleted on a downgrade; creating more than the new plan allows is refused.
  • A missed subscription payment keeps the plan while the processor retries, with included AI paused; an unpaid subscription drops to Hobby limits until it is paid.
  • If checkout says a plan's price is being updated, nothing was charged; try again later.

Included AI usage

  • $1 on hobby, $10 on pro, $100 on enterprise each month. On Hobby it applies only to your personal workspace, and vendor models need the owner's verified email.
  • It resets on the 1st at 00:00 UTC and does not carry over. It is used first; then the Balance pays.
  • AI is priced at each model's list price (shown in the AI Gateway), with no markup. Every answer reports its exact cost.

Included domains

Pro includes 1 standard domain a year and Enterprise 5; a standard domain is one priced at $15 or less. Extra domains, premium names, and domains on Hobby are paid from the Balance. There is no limit on how many you buy.

The Balance

  • Prepaid dollars the owner loads from Dashboard → Billing → Balance: $10 to $1,000 at a time, with the card on file or through checkout (which saves the card).
  • It never expires and is not refundable. Deleting a workspace gives it up only after you confirm.
  • It pays for AI beyond the included amount and for domain purchases, renewals and transfers beyond the included domains.
  • A card is charged only when you choose: an owner paying with the card for one purchase (payWithCard: true), auto-reload, or domain renewal card backup the owner turned on.
  • Every movement — funds added, AI usage, domains, refunds, plan invoices — is a transaction on the Billing page, with receipts for card charges.

Spending limit and alerts

  • Set a monthly spending limit ($1 to $100,000; 0 for none) in Billing → Limits & alerts or PATCH /api/billing/limits { spendLimitCents } (owner). It covers AI beyond the included amount plus domain purchases from the Balance or the card. Past it, requests answer 402 spend_limit_reached.
  • Alerts (alertLevels, default 50, 80 and 100 percent) email the owner once per level per month, for the included AI amount and for the limit.
  • Auto-reload charges the card a chosen amount when available AI credit drops below a threshold, at most 10 times a month. It fires only for requests from a signed-in person unless the owner sets autoReloadForKeys: true.
curl (owner session)
PATCH /api/billing/limits
{ "spendLimitCents": 5000, "alertLevels": [50, 80, 100], "autoReloadForKeys": false }

Seller earnings

Payments you take are Earnings on the project: each sale less the 3.9% + 30¢ fee, held 7 days, then withdrawn by the workspace owner. See Payments.

When things reset

WhatClock
Included AI usageThe 1st of each month, 00:00 UTC
Spending limit, alerts, auto-reload countCalendar month (UTC)
Email quotaRolling 30 days
SubscriptionMonthly from the day you subscribed
Included domainsEach year
Domain renewalsEach domain's own expiry date
Earnings hold7 days after each payment

Who can see and change billing

  • Admins and owners can see billing; only the owner can change the plan, cards, Balance, auto-reload and limits.
  • API keys see the AI credit summary in GET /api/usage (aiCredits) and cannot read or change billing. An owner's AI connector can read the Balance.

Reference

SDK
const { aiCredits } = await hk.usage.get();
// { balance_usd, available_usd, included_usd, included_used_usd, included_remaining_usd, resets_on, buy_url, auto_reload }
// Billing changes are made by the owner in the dashboard (an API key acts as developer).

Usage, activity & audit log

GET /api/usage reports counts against plan limits, the AI credit position, and measured usage for a period. Dashboard → Analytics shows the same data, with a list of what is not measured yet.

Usage

  • ?period= month, last_month, 7d, 30d (default) or 90d, in whole UTC days; ?project=<id> for one project.
  • Returns totals per metric, a daily series, byProject, measured storage and database sizes (with when they were measured), deploy time and build-machine time, and notMeasured.
  • Measured: resource counts, deploys and build time, database queries and rows read and written, storage bytes and object counts (daily), database sizes (daily), memory and vector operations, emails, end-user logins, checkout sessions, agent runs, AI requests, embeddings and AI cost.
  • Not measured yet: requests and bandwidth to sites and apps, server-app running time, function-URL traffic, errors and latency.

What needs attention

GET /api/attention is a ranked to-do list: failed or stuck deploys, domains expiring or waiting on nameservers, failed renewals, refunds owed, plan limits and AI credit running low, failed mail and an unverified email. Each item links to the page that fixes it. The dashboard Overview shows it at the top; the AI connector's needs_attention tool reads it.

Audit log

  • Records changes: creates, deletes, settings, keys, members, money events. Admins read it in Dashboard → Audit Log (filter, export to CSV) or GET /api/orgs/:id/audit-logs.
  • High-volume data calls (queries, memory writes, vector upserts, object deletes, sends, checkout creation, agent runs) are counted in usage instead of written one row each.
  • GET /api/activity is the short recent feed on the Overview; non-admins do not see member, key, billing or account rows.

Reference

SDK
const usage = await hk.usage.get();   // counts, limits, aiCredits, metrics (30 days)

AI connector (MCP)

The connector speaks the Model Context Protocol and signs you in with OAuth. You choose one workspace or all of them, and read-only or read-and-manage access. It then does what your role allows — nothing more — through the same API, with the same limits, audit rows and money checks. About 60 tools cover projects, deploys, repos, databases, storage, domains, payments analytics, AI and docs.

Connect

ClientHow
Claude (web and desktop)Settings → Connectors → Add custom connector → paste https://harakumo.com/mcp → sign in and choose the workspace and access.
Claude Codeclaude mcp add --transport http harakumo https://harakumo.com/mcp
ChatGPTTurn on developer mode (Settings → Apps & Connectors → Advanced), create a connector with the URL https://harakumo.com/mcp and OAuth sign-in. Menu names change between ChatGPT releases.
Other clients (Cursor …){ "mcpServers": { "harakumo": { "url": "https://harakumo.com/mcp" } } }
A headless agentSend an API key instead: Authorization: Bearer hk_live_… (developer-role tools; a read-only key gets read tools).

Access

  • Read only: the assistant can look but not change anything. Read and manage: changes are allowed; tools that change or delete things are marked so the assistant asks you first.
  • A connector acts with your own role, so an admin's connector also gets admin tools such as API keys, members and the audit log; an API key gets developer tools only. Money never leaves through a connector: withdrawals, refunds and payout setup are refused by design.
  • A connection stays signed in and renews itself (the access token lasts 90 days and refreshes; unused for 180 days, it expires). Dashboard → Connectors lists every connection and cuts any of them off immediately.

Sending email from your own domain

Six tools let an assistant set up a sending domain end to end: it adds the domain, shows you the steps and the exact DNS records, checks them while you add them, and sets the project's From address once the domain is verified. See Mail → Send from your own domain.

  • add_mail_domain — start (or start again); returns the records, numbered steps and provider hints.
  • check_mail_domain — check DNS now: each record found, missing or wrong, and "2 of 3 required records found".
  • list_mail_domains and get_mail_domain — status, records and every project's current From address.
  • set_mail_from — a project's From address and Reply-To.
  • remove_mail_domain — stop sending from a domain.
  • Also: host_domain_dns hosts a bought domain's DNS here so every record is added for you, and send_email takes from and replyTo.

New tools not showing?

Assistants remember a connector's tool list from when it was attached. After Harakumo adds tools, disconnect and reconnect the connector, then start a new conversation. The docs themselves are always live through get_docs.

Try asking

  • "What needs my attention?" — one call to needs_attention, most urgent first.
  • "Build a landing page for my bakery and put it online."
  • "Why did the last deploy fail?"
  • "How much of my plan and AI credit is left?"
  • "Buy fruitshop.com and connect it to the store project."
  • "Set up fruitshop.com as a sending domain on Harakumo and walk me through it."

Reference

SDK
// Nothing to install — the connector is the interface.