API Documentation
here lets agents publish websites and store private files in cloud drives. Use Sites to publish HTML, documents, images, PDFs, and static files to live URLs at https://a7een.doy.tech/s/<slug>. Use Drives for private cloud agent storage with presigned uploads, inline reads, and scoped tokens. All publishing and drive routes require an account — sign in by email and use the a7een_… API key shown after the magic link.
Overview
Sites — publish websites and files at https://a7een.doy.tech/s/<slug>.
Drives — private cloud storage for your agents.
Authenticate with Authorization: Bearer <API_KEY> (issued on sign-in) or a logged-in browser session. Drive read routes also accept a scoped Drive token via ?token=.
Install here
Install the here skill so your agent can publish automatically:
# Global install (recommended)
npx skills add heredotnow/skill --skill here-now -g
# Project-local install (no -g flag)
npx skills add heredotnow/skill --skill here-now
# Fallback installer
curl -fsSL https://a7een.doy.tech/install.sh | bashOnce installed, agents can publish with a single script call. Store your API key:
mkdir -p ~/.herenow && echo "API_KEY" > ~/.herenow/credentialsKey resolution order (first match wins): --api-key flag → $HERENOW_API_KEY env → ~/.herenow/credentials file.
Quick start
Publish a site in three steps. You need an API key — sign in by email and copy the a7een_… key from the instructions page.
1. Create site (get presigned URLs)
curl -sS https://a7een.doy.tech/api/v1/publish \
-H "Authorization: Bearer a7een_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"slug": "my-site",
"title": "My Site",
"files": [
{ "path": "index.html", "size": 1234, "hash": "<sha256-of-file>" }
]
}'The response contains siteUrl, presigned upload.uploads[], and a upload.finalizeUrl.
2. Upload changed files
curl -X PUT "<upload.uploads[0].url>" \
-H "Content-Type: application/octet-stream" \
--data-binary @index.html3. Finalize (go live)
curl -sS -X POST "<upload.finalizeUrl>" \
-H "Authorization: Bearer a7een_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "files": [
{ "path": "index.html", "size": 1234, "hash": "<sha256-of-file>" }
] }'Authentication
Two ways to authenticate:
- ● Bearer API key — include Authorization: Bearer <API_KEY>. Keys are issued on sign-in (a7een_…) and verified by SHA-256 hash, so the plain key is shown only once.
- ● Browser session — after signing in via the web app, API requests from the same browser session are authenticated automatically.
- ● Drive read tokens — read-only file and list endpoints accept a scoped Drive token via ?token=drv_… instead of the header.
Authorization: Bearer a7een_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/jsonRequests without a valid key or session return 401 with code unauthenticated or invalid_api_token.
Getting an API key
Sign-in is email-based: submit your email to receive a magic link. Opening the link creates your account (if new), issues an API key, and shows the agent instructions page.
1. Request a magic link
curl -sS -X POST https://a7een.doy.tech/start \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'2. Open the emailed link
The link signs you in, creates your personal team, and issues a fresh a7een_… API key. The key is shown on the /agent/instructions page at that moment — store it somewhere safe, the plain key is not shown again.
Sites
Publish HTML, documents, images, PDFs, videos, and static files to live URLs. The publishing workflow is three steps: create (get presigned URLs) → upload files to S3 → finalize (activate version).
Request body
| Field | Type | Description |
|---|---|---|
| slug | string | required Lowercase letters/numbers/hyphens, 3–32 chars, unique. |
| title | string | optional Site title. |
| description | string | optional Site description. |
| files | array | required Array of { path, hash, size }. |
| files[].path | string | Relative to site root, e.g. index.html, assets/style.css. |
| files[].hash | string | required SHA-256 hex (64 chars). Used for differential uploads — unchanged files land in skipped[]. |
| files[].size | integer | required File size in bytes, max 256 MB. |
curl -sS https://a7een.doy.tech/api/v1/publish \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"slug": "my-site",
"title": "My Site",
"files": [
{ "path": "index.html", "size": 1234,
"hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" },
{ "path": "assets/app.js", "size": 999,
"hash": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3" }
]
}'const res = await fetch('https://a7een.doy.tech/api/v1/publish', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
slug: 'my-site',
files: await Promise.all(fileList.map(async f => ({
path: f.path, size: f.size, hash: await sha256(f),
}))),
}),
});
const data = await res.json();
// Step 2: upload changed files in parallel
await Promise.all(data.upload.uploads.map(async ({ url, headers, path }) =>
fetch(url, { method: 'PUT', headers, body: fileMap[path] })
));
// Step 3: finalize
await fetch(data.upload.finalizeUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ files: fileList }),
});{
"slug": "my-site",
"siteUrl": "https://a7een.doy.tech/s/my-site",
"versionHash": "x7k2Qr9cWt3nYv6f",
"versionId": 12,
"upload": {
"versionId": "12",
"uploads": [{
"path": "index.html",
"method": "PUT",
"url": "https://<presigned-url>",
"headers": { "Content-Type": "application/octet-stream" }
}],
"skipped": ["assets/app.js"],
"finalizeUrl": "https://a7een.doy.tech/api/v1/publish/my-site/finalize",
"expiresInSeconds": 1200
},
"uploadData": { "index.html": { "url": "https://<presigned-url>", "path": "index.html", "hash": "a1b2…" } }
}{
"files": [
{ "path": "index.html", "size": 1234, "hash": "<sha256>" }
]
}{
"message": "Site published successfully",
"slug": "my-site",
"siteUrl": "https://a7een.doy.tech/s/my-site",
"versionHash": "x7k2Qr9cWt3nYv6f",
"previousVersionId": null,
"currentVersionId": "12"
}{
"sites": [{
"slug": "my-site",
"title": "My Site",
"description": null,
"siteUrl": "https://a7een.doy.tech/s/my-site",
"activeVersion": {
"versionHash": "x7k2Qr9cWt3nYv6f",
"files": [{ "path": "index.html", "s3_key": "sites/my-site/versions/.../index.html", "hash": "<sha256>" }]
}
}],
"pagination": { "page": 1, "pageSize": 20, "totalItems": 3, "totalPages": 1 }
}| Field | Type | Description |
|---|---|---|
| title | string | Display title (max 255 chars). |
| description | string | Description (max 5000 chars). |
| is_spa | boolean | Unknown paths fall through to index.html when serving. |
| is_forkable | boolean | Stored only — fork exposure is Hosted only. |
| password_hash | string | Stored only — server-side gating is Hosted only. |
| price | number | Stored only — payment gating is Hosted only. |
{ "message": "Site deleted successfully" }⚡ SPA routing
For single-page apps (React, Vue, Svelte): enable SPA mode so unknown paths serve index.html instead of 404. Static assets still resolve normally — only paths not matching any file fall through to the index.html fallback.
curl -sS -X PATCH https://a7een.doy.tech/api/v1/publish/my-site/metadata \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{ "is_spa": true }'Use root-relative asset paths (/assets/app.js). Vite and Create React App do this by default.
🏢 Features not available on self-hosted instances
The following features exist in the managed here.now SaaS and are referenced by the external documentation. They are not implemented on self-hosted instances like this one — the endpoints below do not exist here and return 404.
- ▸Anonymous sites & claiming — publish without an account, 24-hour expiry, claimToken/claimUrl, POST /publish/{slug}/claim. Self-hosted requires an API key for all publishing.
- ▸Password protection — {"password":…} server-side visitor gating.
- ▸Payment gating — /api/v1/wallet, price metadata, Tempo stablecoin paywall, 402 + paymentSession.
- ▸Forks — is_forkable, POST /publish/{slug}/duplicate, /.herenow/manifest.json, /.herenow/raw/{path}.
- ▸Proxy routes — .herenow/proxy.json server-side upstream proxying with ${VAR_NAME}.
- ▸Account variables — /api/v1/me/variables secret store.
- ▸Custom domains — /api/v1/domains with auto SSL. Sites are served at https://a7een.doy.tech/s/<slug> only.
- ▸Handle & links — /api/v1/handle, /api/v1/links, yourname.a7een.doy.tech subdomains.
Drives
Private cloud folders for agents to store files and share them across sessions. Uploads are single-step: POST /files/uploads returns a presigned S3 PUT URL, and you upload directly to it. Reads return file contents inline. Every account gets a default Drive named Default (5 GB).
{
"path": "notes/report.pdf",
"size": 1024000
}{
"message": "File upload initiated successfully",
"upload": {
"url": "https://<presigned-url>",
"path": "notes/report.pdf",
"expires_in": 1200
}
}# Upload directly to S3 — no server relay
curl -X PUT "<upload.url>" \
--data-binary @report.pdf{
"message": "Files listed successfully",
"files": [{
"key": "notes/report.pdf",
"size": 1024000,
"lastModified": "2026-05-17 10:30:00"
}],
"count": 1
}{
"message": "File read successfully",
"file": { "path": "notes/report.txt", "contents": "<raw file contents>" }
}{
"name": "docs-writer",
"permissions": "read", // "read" | "write" | "read-write"
"path_restriction": "notes/", // optional folder scope
"expires_at": "2026-08-10T12:00:00Z" // optional expiry
}{
"message": "Access token generated successfully",
"token": {
"id": 1,
"name": "docs-writer",
"plain_text_token": "<opaque 64-char secret>",
"permissions": "read",
"path_restriction": "notes/",
"expires_at": "2026-08-10T12:00:00Z"
}
}📤 Sharing & tokens
Use your account API key for full-Drive access. Use scoped Drive tokens when you need narrower access (read-only, path-restricted, time-limited, per-agent). Share the plain_text_token with another agent plus the Drive ID and this API base; the recipient calls read routes with ?token=<token> and must stay inside path_restriction when present.
# Mint a read-only token scoped to notes/ for another agent
curl -sS -X POST https://a7een.doy.tech/api/v1/drives/1/tokens \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{ "name": "docs-writer", "permissions": "read", "path_restriction": "notes/" }'
# Recipient reads a file with the token (no Authorization header needed)
curl https://a7een.doy.tech/api/v1/drives/1/files/notes/report.txt?token=<plain_text_token>Error responses
All errors return JSON with a stable error field plus structured fields for agent recovery.
{
"error": "Rate limit exceeded. Max 60 anonymous sites per hour.",
"code": "rate_limit_exceeded",
"message": "Wait before retrying, or sign in for higher limits.",
"retry_after": 3600,
"docs_url": "https://a7een.doy.tech/docs#limits"
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Fix request body, params, file paths, or JSON syntax before retrying. |
| 401 | unauthorized | Provide Authorization: Bearer or use the anonymous flow. |
| 402 | payment_required | Crypto-gated resource. Response includes paymentSession with createUrl, pollUrl, grantUrl. |
| 403 | forbidden | API key or Drive token lacks access to this resource. |
| 404 | not_found | Check slug, Drive ID, file path, or account ownership. |
| 409 | conflict | Resolve current resource state before retrying (e.g. slug taken). |
| 410 | gone | Resource expired or was deleted; create a new resource. |
| 429 | rate_limit_exceeded | Wait retry_after seconds or follow the Retry-After header. |
| 503 | storage_not_configured | Retry later; contact support if it persists. |
Limits & plans
| Feature | Anonymous | Free | Hobby | Developer |
|---|---|---|---|---|
| Storage | — | 10 GB total | 500 GB total | 2 TB total |
| Sites | — | 500 | 1,000 | Unlimited |
| Drives | — | 1 | 5 | 10 |
| Custom domains | — | 1 | 5 | 20 |
| Handle | — | — | 1 | 1 |
| Max site file size | 250 MB | 5 GB | 5 GB | 5 GB |
| Max Drive file size | — | 500 MB | 500 MB | 500 MB |
| Site expiry | 24 hours | Permanent | Permanent | Permanent |
| Drive version history | — | 7 days | 30 days | 90 days |
| Publish rate limit | 60/hr/IP | 60/hr | 200/hr | 200/hr |
Total storage is shared by published site versions and current Drive files. Drive version history is retained by time window and does not count toward the storage quota.
Agent discovery
here publishes well-known discovery files so agents can find the product, docs, API spec, and skill surfaces without scraping the homepage.
| URL | Description |
|---|---|
| /.well-known/agent.json | Product capabilities, auth model, docs, OpenAPI, and skill links |
| /.well-known/agent-card.json | Agent card describing here skills and capabilities |
| /.well-known/ai-plugin.json | OpenAI-style plugin manifest pointing to /openapi.json |
| /.well-known/api-catalog | RFC 9727 API catalog/linkset |
| /pricing.md | Machine-readable pricing tiers, features, and limits |
| /openapi.json | OpenAPI 3.1 spec (sites, drives, domains, handles, links, variables) |
OpenAPI spec
The stable public API is described by an OpenAPI 3.1 specification. Agents can use it to discover request schemas, response schemas, authentication, and operation IDs. Import into Postman, Insomnia, or your agent's tool registry.