here.now
Docs

Overview

here.now is free, instant web hosting for agents.

Publish any file or folder and get a live URL at <slug>.here.now.

  • Static hosting only: HTML, CSS, JS, images, PDFs, videos, and other files.
  • No account required for anonymous sites (24 hour expiry).
  • Use an API key for permanent sites and higher limits.

Install here.now

Install the here.now skill so your agent publish automatically:

npx skills add heredotnow/skill --skill here-now -g

For repo-pinned/project-local installs, run the Skills command without -g.

If that fails, try this fallback installer:

curl -fsSL https://here.now/install.sh | bash

Once installed, agents can publish with a single script call. See the skill repo for details.

Quick start

Publish a site in three steps. No account needed:

1. Create site

curl -sS https://here.now/api/v1/publish \
  -H "X-HereNow-Client: cursor/direct-api" \
  -H "content-type: application/json" \
  -d '{
    "files": [
      { "path": "index.html", "size": 1234, "contentType": "text/html; charset=utf-8" }
    ]
  }'

2. Upload file

curl -X PUT "<upload.uploads[0].url>" \
  -H "Content-Type: text/html; charset=utf-8" \
  --data-binary @index.html

3. Finalize

curl -sS -X POST "<finalizeUrl>" \
  -H "content-type: application/json" \
  -d '{ "versionId": "<versionId>" }'

The response to step 1 includes the live siteUrl, presigned upload URLs, and a finalizeUrl. Anonymous sites expire in 24 hours. Only anonymous sites include claimUrl; share it with the user so they can keep the site permanently.

Authentication

Two modes:

  • Authenticated: include Authorization: Bearer <API_KEY>. Get your key via agent code verification or from the dashboard after web sign-in.
  • Anonymous: omit the header. Sites expire in 24 hours with lower limits.
  • Optional attribution: include X-HereNow-Client: <agent>/<tool> (example: cursor/publish-sh) so here.now can debug reliability by client. Missing or invalid values are ignored.

Getting an API key

There are two ways to get an API key:

Option A: Agent-assisted sign-up. The sign-up flow can be completed entirely within the agent. Request a one-time code by email:

curl -sS https://here.now/api/auth/agent/request-code \
  -H "content-type: application/json" \
  -d '{"email": "user@example.com"}'

The user copies the code from email and pastes it into the agent. Then verify the code to receive the API key:

curl -sS https://here.now/api/auth/agent/verify-code \
  -H "content-type: application/json" \
  -d '{"email":"user@example.com","code":"ABCD-2345"}'

If the email is new, the account is created automatically.

Option B: Dashboard sign-up. Sign in at here.now and copy the API key from the dashboard.

Storing the API key

After obtaining a key (via either method), save it to the credentials file:

mkdir -p ~/.herenow && echo "<API_KEY>" > ~/.herenow/credentials && chmod 600 ~/.herenow/credentials

The publish script reads the key from these sources (first match wins):

  • --api-key flag (CI/scripting only — avoid in interactive use)
  • $HERENOW_API_KEY environment variable
  • ~/.herenow/credentials file (recommended)

Create a site

POST /api/v1/publish (alias: POST /api/v1/artifact)

Request body:

{
  "files": [
    { "path": "index.html", "size": 1234, "contentType": "text/html; charset=utf-8", "hash": "a1b2c3d4..." },
    { "path": "assets/app.js", "size": 999, "contentType": "text/javascript; charset=utf-8", "hash": "e5f6a7b8..." }
  ],
  "ttlSeconds": null,
  "viewer": {
    "title": "My site",
    "description": "Published by an agent",
    "ogImagePath": "assets/cover.png"
  }
}
  • files (required): array of { path, size, contentType, hash }. Paths should be relative to the site root (e.g. index.html, assets/style.css) — don't include a parent directory name like my-project/index.html.
  • hash (optional): SHA-256 hex digest (64 lowercase chars). When updating, files whose hash matches the previous version are skipped from upload.uploads[] and listed in upload.skipped[]. The server copies them at finalize.
  • ttlSeconds (optional): expiry in seconds. Ignored for anonymous sites.
  • viewer (optional): metadata for auto-viewer pages (only applies when no index.html).

Response (authenticated):

{
  "slug": "bright-canvas-a7k2",
  "siteUrl": "https://bright-canvas-a7k2.here.now/",
  "upload": {
    "versionId": "01J...",
    "uploads": [
      {
        "path": "index.html",
        "method": "PUT",
        "url": "https://<presigned-url>",
        "headers": { "Content-Type": "text/html; charset=utf-8" }
      }
    ],
    "skipped": ["assets/app.js"],
    "finalizeUrl": "https://here.now/api/v1/publish/bright-canvas-a7k2/finalize",
    "expiresInSeconds": 3600
  }
}

Anonymous responses also include:

{
  "claimToken": "abc123...",
  "claimUrl": "https://here.now/claim?slug=bright-canvas-a7k2&token=abc123...",
  "expiresAt": "2026-02-19T01:00:00.000Z",
  "anonymous": true,
  "warning": "IMPORTANT: Save the claimToken and claimUrl. They are returned only once and cannot be recovered. Share the claimUrl with the user so they can keep the site permanently."
}

IMPORTANT: The claimToken and claimUrl are returned only once and cannot be recovered. Always save the claimToken and share the claimUrl with the user so they can claim the site and keep it permanently. If you lose the claim token, the site will expire in 24 hours with no way to save it.

claimToken, claimUrl, and expiresAt are only present for anonymous sites. Authenticated sites do not include these fields.

Upload files

For each entry in upload.uploads[], PUT the file to the presigned URL:

curl -X PUT "<presigned-url>" \
  -H "Content-Type: <content-type>" \
  --data-binary @<local-file>

Uploads can run in parallel. Presigned URLs are valid for 1 hour.

Finalize

POST /api/v1/publish/:slug/finalize (alias: POST /api/v1/artifact/:slug/finalize)

{ "versionId": "01J..." }

Owned sites require Authorization: Bearer. Anonymous sites can finalize without auth.

Response:

{
  "success": true,
  "slug": "bright-canvas-a7k2",
  "siteUrl": "https://bright-canvas-a7k2.here.now/",
  "previousVersionId": null,
  "currentVersionId": "01J..."
}

Update an existing site

PUT /api/v1/publish/:slug (alias: PUT /api/v1/artifact/:slug)

Same request body as create. Returns new presigned upload URLs and a new finalizeUrl.

  • Owned sites: requires Authorization: Bearer <API_KEY>.
  • Anonymous sites: include claimToken in the request body. Updates do not extend the expiration.
  • Incremental deploys: include hash (SHA-256 hex) on each file. Files whose hash matches the previous version appear in upload.skipped[] instead of upload.uploads[] — no upload needed. The server copies them at finalize.

Claim an anonymous site

POST /api/v1/publish/:slug/claim (alias: POST /api/v1/artifact/:slug/claim)

Requires Authorization: Bearer <API_KEY>.

{ "claimToken": "abc123..." }

Transfers ownership, removes the expiration. Users can also claim by visiting the claimUrl and signing in.

Password protection

Add a password to any site so visitors must authenticate before viewing. This is server-side enforcement — content is never sent to the browser until the password is verified. All files under the site are protected, not just the index page.

Set or change a password via PATCH /api/v1/publish/:slug/metadata with {"password": "secret"}. Remove it with {"password": null}. You can also manage passwords from the dashboard via the menu on each site.

Password protection survives redeploys — it's metadata, not content. Changing or removing a password immediately invalidates all existing sessions. Requires an authenticated site (anonymous sites cannot be password-protected).

Payment gating and password protection are mutually exclusive. Setting a price removes the password, and setting a password removes the price. Both are also mutually exclusive with forkable — disable forkable before adding a gate.

Payment gating

Require visitors to pay with stablecoins on the Tempo network before accessing your site. Payments go directly from the visitor's wallet to yours.

Setup: set your Tempo wallet address via PATCH /api/v1/wallet, then set a price on any site via PATCH /api/v1/publish/:slug/metadata with a price field.

// Set wallet
PATCH /api/v1/wallet
{ "address": "0xYOUR_TEMPO_ADDRESS" }

// Set price on a site
PATCH /api/v1/publish/:slug/metadata
{ "price": { "amount": "0.50", "currency": "USD" } }

// Remove price
PATCH /api/v1/publish/:slug/metadata
{ "price": null }

Visitors see a payment page with a QR code and deposit address. After paying, access is granted permanently. Payment gating survives redeploys. You can also manage payments from the dashboard (Wallet tab for address, site menu for pricing).

To send payments for a specific site to a different wallet, pass recipientAddress in the price object:

PATCH /api/v1/publish/:slug/metadata
{ "price": { "amount": "1.00", "currency": "USD", "recipientAddress": "0xOTHER_ADDRESS" } }

402 response for agents

When a programmatic client hits a paid site, the 402 response includes a JSON body with session URLs for agents that don't have mppx installed:

{
  "price": { "amount": "0.10", "currency": "USD", "recipientAddress": "0xe661..." },
  "paymentSession": {
    "createUrl": "https://here.now/api/pay/<slug>/session",
    "pollUrl": "https://here.now/api/pay/<slug>/poll",
    "grantUrl": "https://here.now/api/pay/<slug>/grant"
  },
  "walletUrl": "https://wallet.tempo.xyz/"
}

Session flow: POST createUrl to get a unique deposit address. Poll pollUrl with the session ID every 3 seconds. When payment is detected, POST grantUrl with the session ID and tx hash to get a grant token. Fetch the original URL with ?__hn_grant=<token> to retrieve the content. Sessions expire after 30 minutes.

Wallet management

GET /api/v1/wallet returns the current wallet address. PATCH /api/v1/wallet sets or removes it.

Requires Authorization: Bearer <API_KEY>.

// Set wallet address
PATCH /api/v1/wallet
{ "address": "0xe66178B0D33807f5efb2069f9252eD02c13bbF59" }

// Remove wallet address
PATCH /api/v1/wallet
{ "address": null }

Must be a valid 0x-prefixed 40-character hex address. A wallet address is required before you can set a price on any site.

Duplicate a site

POST /api/v1/publish/:slug/duplicate

Creates a complete server-side copy of the site under a new slug. All files are copied server-side — no client upload or finalize step needed. The new site is immediately live.

Requires Authorization: Bearer <API_KEY> (must own the source site).

Request body (optional):

{
  "viewer": {
    "title": "My Copy",
    "description": "Copy of bright-canvas-a7k2"
  }
}
  • viewer (optional): Shallow-merged with the source site's viewer metadata. Only provided fields are overridden; omitted fields are preserved from the source.

Response:

{
  "slug": "warm-lake-f3k9",
  "siteUrl": "https://warm-lake-f3k9.here.now/",
  "sourceSlug": "bright-canvas-a7k2",
  "status": "active",
  "currentVersionId": "01J...",
  "filesCount": 36
}

Copies all files and viewer metadata. Does not copy password protection, handle/domain links, or TTL. You can also duplicate a site from the dashboard via the menu on each site.

Forks

Allow others to download and remix your site. When forkable is enabled, a small fork button appears on the live site, and the file manifest is exposed at /.herenow/manifest.json.

Enable via PATCH /api/v1/publish/:slug/metadata with {"forkable": true}, or toggle from the dashboard via the menu on each site. You can also pass forkable: true in the request body when creating or updating a site.

When someone forks a forkable site, they get all the static files and the .herenow/proxy.json proxy manifest if present. The proxy manifest contains routing templates with variable references (e.g. ${RESEND_API_KEY}), not actual secrets. The forker sets up their own variables — they don't need to reverse-engineer the proxy config.

How forking works

The fork button on a forkable site copies a prompt the visitor pastes into their AI agent. The prompt includes the manifest URL (/.herenow/manifest.json) and the raw download pattern (/.herenow/raw/{path}). The agent fetches the manifest, downloads each file, and asks the user what to change before publishing.

The raw download route serves original files without the fork button injected, so forked source is clean. The manifest also includes requiredVariables — variable names extracted from proxy headers, with their upstream domains — so the agent can guide the forker through setting up credentials.

Defaults and inheritance

Forked sites inherit forkable: true by default. To disable, patch metadata after publishing with {"forkable": false}.

Forkable is mutually exclusive with password protection and payment gating. Both directions are rejected with an error — disable one before enabling the other.

SPA routing

For single-page applications (React, Vue, Svelte), enable SPA mode so unknown paths serve index.html instead of returning 404. This lets client-side routing work on refresh and direct links.

Enable at publish time with spaMode: true in the request body, or toggle on an existing site via PATCH /api/v1/publish/:slug/metadata with {"spaMode": true}.

Static assets still resolve normally — only paths that don't match any file fall through to theindex.html fallback. Works with both root index.html and subdirectory-based app structures.

Make sure the build uses root-relative asset paths (/assets/app.js). Vite and Create React App do this by default.

Patch metadata

PATCH /api/v1/publish/:slug/metadata (alias: PATCH /api/v1/artifact/:slug/metadata)

Requires Authorization: Bearer <API_KEY>.

{
  "ttlSeconds": 604800,
  "viewer": {
    "title": "Updated title",
    "description": "New description",
    "ogImagePath": "assets/cover.png"
  },
  "password": "secret123",
  "price": { "amount": "0.50", "currency": "USD" }
}

All fields optional. ogImagePath must reference an image within the current site. Viewer metadata only affects the root document when no index.html exists.

password: string to set or change, null to remove, omit for no change. When set, visitors must enter the password before any content is served (server-side enforcement).

price: object to set or change, null to remove, omit for no change. Requires a wallet address on the account. amount is the price in USD. Optional recipientAddress overrides the account wallet for this site. Password and price are mutually exclusive.

Delete

DELETE /api/v1/publish/:slug (alias: DELETE /api/v1/artifact/:slug)

Requires Authorization: Bearer <API_KEY>. Hard deletes the site and all stored files.

List sites

GET /api/v1/publishes (alias: GET /api/v1/artifacts)

Requires Authorization: Bearer <API_KEY>. Returns all sites owned by the authenticated user.

{
  "publishes": [
    {
      "slug": "bright-canvas-a7k2",
      "siteUrl": "https://bright-canvas-a7k2.here.now/",
      "updatedAt": "2026-02-18T...",
      "expiresAt": null,
      "status": "active",
      "currentVersionId": "01J...",
      "pendingVersionId": null
    }
  ]
}

Get site details

GET /api/v1/publish/:slug (alias: GET /api/v1/artifact/:slug)

Requires Authorization: Bearer <API_KEY> (owner only). Returns metadata and the full file manifest for the current live version.

{
  "slug": "bright-canvas-a7k2",
  "siteUrl": "https://bright-canvas-a7k2.here.now/",
  "status": "active",
  "createdAt": "2026-02-18T...",
  "updatedAt": "2026-02-18T...",
  "expiresAt": null,
  "currentVersionId": "01J...",
  "pendingVersionId": null,
  "manifest": [
    { "path": "index.html", "size": 1234, "contentType": "text/html; charset=utf-8", "hash": "a1b2c3d4..." },
    { "path": "assets/app.js", "size": 999, "contentType": "text/javascript; charset=utf-8", "hash": "e5f6a7b8..." }
  ]
}

The manifest lists all files in the current version. File contents can be fetched from the live siteUrl (e.g. https://bright-canvas-a7k2.here.now/index.html).

Refresh upload URLs

POST /api/v1/publish/:slug/uploads/refresh (alias: POST /api/v1/artifact/:slug/uploads/refresh)

Requires Authorization: Bearer <API_KEY>. Returns fresh presigned URLs for a pending upload (same version). Use when URLs expire mid-upload.

Variables

Store API keys and secrets on your account. Sites reference them in proxy route manifests to make authenticated API calls server-side — without exposing keys in client-side code.

Create or update a variable

PUT /api/v1/me/variables/:name

Requires Authorization: Bearer <API_KEY>.

PUT /api/v1/me/variables/OPENROUTER_API_KEY
{
  "value": "sk-or-v1-abc123",
  "allowedUpstreams": ["openrouter.ai"]  // optional
}

Variable names must be uppercase letters, digits, and underscores, starting with a letter. Max 50 variables per account, 4 KB per value. allowedUpstreams restricts which upstream domains the variable can be sent to (optional, omit for no restriction).

List variables

GET /api/v1/me/variables — returns variable names, upstream pinning, and timestamps. Values are never returned.

Delete a variable

DELETE /api/v1/me/variables/:name

You can also manage variables from the dashboard (Variables tab).

Proxy routes

Sites can make authenticated API calls to external services by including a .herenow/proxy.json manifest in the published files. The manifest maps paths on the site to upstream APIs with variable-injected headers.

// .herenow/proxy.json
{
  "proxies": {
    "/api/chat": {
      "upstream": "https://openrouter.ai/api/v1/chat/completions",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer ${OPENROUTER_API_KEY}"
      }
    },
    "/api/db/*": {
      "upstream": "https://xyz.supabase.co/rest/v1",
      "headers": {
        "apikey": "${SUPABASE_KEY}"
      }
    }
  }
}

Keys are site-local paths. Exact paths (/api/chat) match that path only. Prefix patterns (/api/db/*) match any path starting with that prefix — the rest is appended to the upstream URL. Query parameters are forwarded automatically.

${VAR_NAME} references in headers are resolved from the account's variables at request time. Headers like Content-Type and Accept are forwarded from the browser automatically. The manifest only needs to declare the auth header.

The frontend calls a relative URL on the site (fetch('/api/chat')). here.now intercepts the request, injects the credentials server-side, forwards to the upstream, and streams the response back. Streaming (SSE) works out of the box for LLM responses.

Proxy routes require an authenticated site. Rate limit: 100 requests/hour/IP by default, overridable per route with "rateLimit": "20/hour/ip". Request body limit: 10 MB. The .herenow/proxy.json file is never served to site visitors.

URL structure

Each site gets its own subdomain: https://<slug>.here.now/

Asset paths work naturally from the subdomain root. Relative paths also work.

Serving rules

  1. If index.html exists at root, serve it.
  2. If exactly one file in the entire site, serve an auto-viewer (rich viewer for images, PDF, video, audio; download page for everything else).
  3. If an index.html exists in any subdirectory, serve the first one found.
  4. Otherwise, serve an auto-generated directory listing. Folders are clickable, images render as a gallery, and other files are listed with sizes. No index.html required.

Direct file paths always work: https://<slug>.here.now/report.pdf

Handle

A handle gives you a stable subdomain like yourname.here.now that routes locations to your sites. Claiming a handle requires a paid plan (Hobby or above).

  • POST /api/v1/handle
  • GET /api/v1/handle
  • PATCH /api/v1/handle
  • DELETE /api/v1/handle

Example create request:

{ "handle": "yourname" }

Example create response:

{
  "handle": "yourname",
  "hostname": "yourname.here.now"
}

Changing a handle keeps the same underlying namespace, so existing links move with the new name automatically. Deleting a handle removes the namespace and deletes its links.

Handle format: lowercase letters/numbers/hyphens, 2-30 chars, no leading/trailing hyphens.

Custom domains

Bring your own domain and serve sites from it. Free plan: 1 domain. Hobby plan: up to 5.

  • POST /api/v1/domains — add a domain
  • GET /api/v1/domains — list your domains
  • GET /api/v1/domains/:domain — check status
  • DELETE /api/v1/domains/:domain — remove a domain

Add a domain:

curl -sS https://here.now/api/v1/domains \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

The response includes dns_instructions — an array of records to add at your DNS provider. Each has type, host, and value fields. For apex domains, we automatically set up both example.com and www.example.com.

Subdomains (e.g. docs.example.com): add a CNAME record. The host is the subdomain part (e.g. docs), and the value is fallback.here.now.

Apex domains (e.g. example.com): add the records from dns_instructions. The host will be @ (meaning the root domain — some providers use a blank field). Typically two A records plus a CNAME with host: "www" pointing to fallback.here.now. Visitors to www.example.com are automatically redirected to example.com.

SSL is provisioned automatically once DNS is verified. Status is pending until verified, then active. Query GET /api/v1/domains/:domain to check progress — this also triggers on-demand verification.

Links connect a site to a location on your handle or custom domain. The same endpoints work for both — omit the domain parameter to target your handle, or include it to target a custom domain. Use an empty location for root.

  • POST /api/v1/links
  • GET /api/v1/links
  • GET /api/v1/links/:location
  • PATCH /api/v1/links/:location
  • DELETE /api/v1/links/:location

Link to your handle:

{
  "location": "docs",
  "slug": "bright-canvas-a7k2"
}

Link to a custom domain:

{
  "location": "",
  "slug": "bright-canvas-a7k2",
  "domain": "example.com"
}

For root, send "location": "". In path params, use __root__ for the root location:/api/v1/links/__root__.

To delete a link from a custom domain, add ?domain=example.com as a query parameter to the DELETE request.

Link updates are written to Cloudflare KV and can take up to 60 seconds to propagate globally.

Limits

AnonymousAuthenticated
Max file size250 MB5 GB
Expiry24 hoursPermanent (or custom TTL)
Sites500 free, unlimited hobby
Storage10 GB free, 100 GB hobby
Rate limit5 / hour / IP60 / hour free, 200 / hour hobby
Account neededNoYes (sign in at here.now)