# API reference

Four anonymous calls that give an agent a temporary upload link, a shared page, a webhook URL or a website, with analytics on every link. No account, no API key, gone in 24 hours.

Last updated: 21 October 2026

## For agents

Start with the [skill](https://hello.new/skill.md) or the [llms.txt index](https://hello.new/llms.txt). This reference is also [plain Markdown](https://hello.new/docs.md), the whole API is described at [api.hello.new/openapi.json](https://api.hello.new/openapi.json), and capabilities are listed at [/.well-known/agent.json](https://hello.new/.well-known/agent.json).

Agents in ChatGPT, Codex, Claude Code, Cursor or VS Code can use the same links as tools through the [MCP server](https://hello.new/docs/mcp).

## The shape of every call

The API base is `https://api.hello.new`. Requests and responses are JSON (a site may also arrive as an archive). There is no authentication to create a resource. Each resource comes back with:

- `url`: the address you hand to a person or a service.
- `key`: a secret for reading the resource back. It is shown once. Send it as `?key=` or `Authorization: Bearer`. Only this link key goes in `?key=`; account keys and session tokens go in the header.
- `expires_at`: when everything about the resource is deleted. Default 24 hours; pass `expires_in` (seconds, 60 to 86400) for less. `null` on account links that never expire.
- `claim_url`: anonymous links only. Give it to your user if they want to keep the link; see [Accounts](#accounts).
- `DELETE` the resource with the key when you are done, and it goes early.

Errors are `{ "error": "a sentence" }` with the usual status: 401 wrong key, 403 needs an account or the account is full, 404 unknown or expired, 409 full or name taken, 413 too large, 422 bad input, 429 slow down.

Send `X-Hello-Client: <your agent>` (for example `claude-code`, `codex`, `cursor`, `openclaw`, `chatgpt`). It is optional, and it is the only telemetry there is.

## Upload

A link a person opens to send files to your agent.

```sh
curl -X POST https://api.hello.new/upload \
  -H 'Content-Type: application/json' \
  -H 'X-Hello-Client: claude-code' \
  -d '{"note": "Send me the logs from last night", "max_files": 3}'
```

```json
{
  "kind": "upload",
  "id": "k7m2x9pq",
  "url": "https://hello.new/u/k7m2x9pq",
  "key": "…",
  "files_url": "https://api.hello.new/u/k7m2x9pq/files?key=…",
  "note": "Send me the logs from last night",
  "max_files": 3,
  "max_bytes": 52428800,
  "created_at": "2026-09-23T09:14:02Z",
  "expires_at": "2026-09-24T09:14:02Z"
}
```

Body fields, all optional: `note` (up to 280 characters, shown on the page), `max_files` (1 to 10, default 10), `expires_in`.

Give `url` to the person. When they have uploaded, list the files with the key:

```sh
curl 'https://api.hello.new/u/k7m2x9pq/files?key=…'
```

```json
{
  "kind": "upload",
  "id": "k7m2x9pq",
  "note": "Send me the logs from last night",
  "expires_at": "2026-09-24T09:14:02Z",
  "files": [
    {
      "seq": 1,
      "name": "app.log",
      "size": 48213,
      "content_type": "text/plain",
      "received_at": "2026-09-23T09:20:41Z",
      "url": "https://hellouploads.com/u/k7m2x9pq/files/1?key=…"
    }
  ]
}
```

Download a file from its `url`; the response carries the stored content type and a `Content-Disposition` with the original name. Uploaded files are served from `hellouploads.com`, a domain of their own that serves nothing else. Asking `api.hello.new` for one redirects there. If you listed the files with an account key instead of the link key, the file URLs carry no key: send the same `Authorization` header to download. Poll the list every few seconds while you wait, or ask the person to tell you when they are done.

You can also upload without the page. Anyone with the id can `PUT` a raw body:

```sh
curl -X PUT 'https://api.hello.new/u/k7m2x9pq/files/app.log' \
  -H 'Content-Type: text/plain' --data-binary @app.log
```

`GET /u/{id}` without a key returns the note, limits and how many files have arrived. That is what the page shows. `DELETE /u/{id}?key=…` removes everything.

Limits: 50 MB a file, 10 files a link.

## Share

A page that shows text or Markdown your agent produced.

```sh
curl -X POST https://api.hello.new/share \
  -H 'Content-Type: application/json' \
  -d '{"title": "Q3 summary", "text": "# Q3\n\nRevenue up 12%…"}'
```

```json
{
  "kind": "share",
  "id": "b4nq8wzx",
  "url": "https://hello.new/s/b4nq8wzx",
  "raw_url": "https://hellouploads.com/s/b4nq8wzx/raw",
  "key": "…",
  "title": "Q3 summary",
  "content_type": "text/markdown",
  "created_at": "2026-09-23T09:14:02Z",
  "expires_at": "2026-09-24T09:14:02Z"
}
```

Body fields: `text` (required), `title` (up to 120 characters), `content_type` (`text/markdown`, the default, or `text/plain`), `expires_in`.

Anyone with the link can read it. `GET /s/{id}` returns the JSON above with the text included; `raw_url` returns the text itself with its content type, which is what another agent should fetch. Like uploaded files, it is served from hellouploads.com, never hello.new. `DELETE /s/{id}?key=…` removes it.

Limit: 256 KB of text.

## Webhook

A URL that accepts any HTTP request and keeps it for you to read.

```sh
curl -X POST https://api.hello.new/webhook \
  -H 'Content-Type: application/json' \
  -d '{"note": "Stripe test events"}'
```

```json
{
  "kind": "webhook",
  "id": "f3gd7hxm",
  "url": "https://api.hello.new/w/f3gd7hxm",
  "key": "…",
  "inbox_url": "https://api.hello.new/webhook/f3gd7hxm?key=…",
  "note": "Stripe test events",
  "max_requests": 100,
  "max_bytes": 262144,
  "created_at": "2026-09-23T09:14:02Z",
  "expires_at": "2026-09-24T09:14:02Z"
}
```

Give `url` to the service. Every method is accepted, on `/w/{id}` and any path beneath it (`/w/{id}/stripe`, `/w/{id}/github/push`). The sender gets `200 { "ok": true, "seq": 1 }`.

Read what arrived:

```sh
curl 'https://api.hello.new/webhook/f3gd7hxm?key=…&since=0&wait=25'
```

```json
{
  "kind": "webhook",
  "id": "f3gd7hxm",
  "note": "Stripe test events",
  "expires_at": "2026-09-24T09:14:02Z",
  "requests": [
    {
      "seq": 1,
      "received_at": "2026-09-23T09:31:07Z",
      "method": "POST",
      "path": "stripe",
      "query": "",
      "headers": { "content-type": "application/json", "stripe-signature": "t=…" },
      "content_type": "application/json",
      "body": "{\"type\":\"payment_intent.succeeded\"}"
    }
  ]
}
```

Headers come back as sent, except `cookie`, `host` and proxy headers (`cf-*`, `x-forwarded-*` and the like), which are not kept. `since` returns only requests with a larger `seq`, so keep the last one you saw. `wait` (up to 25 seconds) holds the request open until something new arrives, then returns it; an empty `requests` list after the wait means nothing came, not an error. Loop on that to wait for a callback. Bodies that are not UTF-8 arrive as `body_base64` instead of `body`. `DELETE /webhook/{id}?key=…` removes it.

Limits: 100 requests kept, 256 KB each. The sender is not rate limited; the cap is.

## Site

A website your agent built, at an address of its own. Static files only: HTML, CSS, JavaScript, images and fonts.

```sh
curl -X POST https://api.hello.new/site \
  -H 'Content-Type: application/json' \
  -d '{"title": "Bakery", "files": {"index.html": "<!doctype html><title>Bakery</title><link rel=stylesheet href=style.css><h1>Fresh bread</h1>", "style.css": "h1 { color: tomato }"}}'
```

```json
{
  "kind": "site",
  "id": "k7m2x9pq",
  "url": "https://k7m2x9pq.hellonew.app",
  "key": "…",
  "title": "Bakery",
  "files": 2,
  "bytes": 112,
  "protected": false,
  "created_at": "2026-10-03T09:14:02Z",
  "expires_at": "2026-10-04T09:14:02Z"
}
```

`files` maps each path to its contents: text as a string, anything binary as `{"base64": "…"}`. `index.html` is the home page. The other body fields are `title` (up to 120 characters), `expires_in`, and on an account `name` and `password` (below).

A built app, such as a React or Vite project, goes up as its build folder in one archive:

```sh
npm run build
tar -cz -C dist . | curl -X POST 'https://api.hello.new/site?title=Bakery' --data-binary @-
```

Send a `.tar` or `.tar.gz`, with `title`, `expires_in` and `name` in the query string. Directories, links, `.DS_Store` and `._` files are left out. Next.js works with `output: 'export'`; anything that needs a server (server rendering, API routes, server actions) does not.

Paths are served the way Cloudflare Pages serves them:

- `/about` serves `about.html`, or redirects to `/about/` when there is an `about/index.html`.
- A path with no file serves `404.html` with a 404. Without a `404.html` it serves `index.html`, so a single-page app's routes survive a reload.
- Every site has a subdomain to itself, so links to `/assets/…` from the root work as built.

`PUT /site/{id}?key=…` with the same kind of body publishes again at the same address: the files you send replace every old one. `GET /site/{id}` describes the site. `DELETE /site/{id}?key=…` removes it. On an account, a `name` also gives the site `hello.new/@you/{name}`, which sends visitors on to it.

Everything in a site is public, including any key built into its JavaScript; with a password, it is open to everyone who has the password. To collect what visitors submit, post it to a [webhook](#webhook): browser code may call one directly, and your agent reads the entries. Sites made without an account carry a small "Hosted by hello.new" badge with a report link, ask search engines not to index them, and cannot contain a password field. Keep one with its `claim_url`, or publish with an account key, and it is served exactly as published.

A site on an account can ask for a password. Add `password` (4 to 128 characters; spaces at either end are dropped) to the JSON body, never to the URL:

```sh
curl -X POST https://api.hello.new/site \
  -H 'Authorization: Bearer hk_…' \
  -H 'Content-Type: application/json' \
  -d '{"title": "Q3 plan", "expires_in": 86400, "password": "plum orbit 42", "files": {"index.html": "…"}}'
```

The response says `"protected": true`, and so does `GET /site/{id}`. Every address of the site then answers with hello.new's own password page, and none of the site's files, until the visitor enters the password. After that their browser stays in for 24 hours, or until the site expires if that is sooner. A visitor gets 10 tries every 15 minutes. Send the password to people separately from the link. A protected site is never cached or indexed.

`PUT /site/{id}` keeps the password unless its JSON body has one: a new `password` replaces it, and everyone who got in with the old one has to enter the new one; `"password": null` removes it. Anonymous sites can't have a password.

Limits: 10 MB and 100 files a site, 25 MB a file. A JSON body can be up to 20 MB; send bigger sites as an archive.

## Analytics

Every create response has an `analytics_url`: `GET /analytics/{id}?key=…` (or the owning account's `Authorization` header) says what happened to the link, so you can tell your user. It also has a `stats_url`, `https://hello.new/stats#id=…&key=…`: the same numbers as a page with a chart, for your user to open themselves. The key rides in the URL fragment, which browsers never send to a server.

```sh
curl -s 'https://api.hello.new/analytics/b4nq8wzx?key=…'
```

```json
{
  "id": "b4nq8wzx",
  "kind": "share",
  "full": false,
  "interval": "24h",
  "unit": "hour",
  "totals": { "views": 12, "visitors": 7, "reads": 2, "uploads": 0, "downloads": 0, "requests": 0,
              "refused": 1, "bots": 3, "countries": 3, "bytes": 0 },
  "timeseries": [ { "start": "2026-09-29T08:00:00.000Z", "views": 4, "visitors": 3, "reads": 0, "…": 0 } ],
  "countries": [ { "value": "AU", "hits": 8, "visitors": 5 } ],
  "locked": ["regions", "cities", "continents", "devices", "browsers", "os", "referrers", "…", "events"],
  "claim_url": "https://hello.new/claim#id=b4nq8wzx&key=…"
}
```

- **What counts.** `views` are people opening the page (the upload page or the shared page), `visitors` the distinct people among them, `reads` agents reading the link's JSON or raw text, `uploads` and `downloads` files in and out, `requests` calls to a webhook. `refused` is anything turned away, such as a view after the link expired or a download past the limit. Crawlers and link previews count as `bots`, not views.
- **The response** says which link it is (`url`, `title`, `note`, `owner`, `name`, `created_at`, `expires_at`) as well as what happened to it.
- **Anonymous links** show their whole 24 hours as totals, an hourly `timeseries` and the top five `countries`. The other breakdowns are in `locked`: every request is recorded in full from the start, and claiming the link (`claim_url`) opens all of it.
- **Account links** add `regions`, `cities`, `continents`, `devices`, `browsers`, `os`, `referrers`, `utm_sources`, `utm_mediums`, `utm_campaigns`, `languages`, `networks` (the visitor's ISP or host) and `agents` (reads by `X-Hello-Client`), each as `{ value, hits, visitors }`, and `events`, the latest 100 requests with their time, place, device, referrer and status (never an IP address, headers or anything else that singles a person out). Pick the window with `interval`: `24h`, `7d`, `30d` (the default), `90d` or `all`.
- **Where visitors came from.** A link's own page passes on its referrer and any `utm_*` tags, so add them to the URL you hand out (`https://hello.new/s/b4nq8wzx?utm_source=newsletter`) to tell your channels apart.
- **How long.** An anonymous link's analytics go with it after 24 hours; claim it to keep both.

## Accounts

Anonymous links are the default and need nothing. An account adds a username and links that stay.

- **Username.** Claim one on the [front page](/). It becomes the prefix of every account link: `hello.new/@sam/…`.
- **Account key.** Make one on the front page after claiming, and give it to your agent. Send it as `Authorization: Bearer hk_…` on any create call. The response then has `owner` and `name`, `expires_at` is `null` unless you pass `expires_in`, and the link lives at `hello.new/@sam/<name or id>` (webhooks at `api.hello.new/@sam/<name or id>`). The account key also works in place of a link `key` for that user's links, as a Bearer header only.
- **Header only.** Account keys and session tokens go in `Authorization: Bearer …`, never in the URL. In `?key=` they get `401`: "Only a link key goes in ?key=. Send account keys and session tokens in the Authorization header: Authorization: Bearer <token>." File lists read with an account credential return file URLs without a key.
- **Quota.** An account keeps at most 1000 live links and 1 GB of stored files, share text and webhook bodies, or 100,000 and 100 GB on [Pro](#plans). A new link past the limit gets `403`; anything that would pass the storage limit gets `413`. Delete links to make room.
- **`name`.** Optional on account links: 2 to 30 lowercase letters, numbers and dashes. Unique per user across links and agent addresses; `409` if taken. Anonymous calls with a `name` get `403`.
- **Changing a username.** Only while no links or agent addresses use it. Otherwise `409`: remove them first.
- **Claiming.** `POST /claim` with `{ "id", "key", "name"? }` and a signed-in session moves an anonymous link onto the account for good. It returns `kind`, `id`, `url`, `name` and `expires_at` (`null`). The link keeps its key, so the agent that made it can still read it, and the old address keeps working. Claiming counts against the account quota. People do this from the `claim_url` page, which keeps the key out of server logs by carrying it in the URL fragment.
- **Lookup.** `GET /at/{username}/{name}` says what lives at an address without counting as a webhook call.

A link only ever resolves under the username that owns it.

## Plans

Every account is on the free plan until it pays for Pro. Anonymous links get the free plan's limits per link.

| | Free | Pro |
|---|---|---|
| Live links | 1,000 | 100,000 |
| Storage | 1 GB | 100 GB |
| File size | 50 MB | 100 MB |
| Files per upload link | 10 | 100 |
| Requests kept per webhook | 100 | 10,000 |
| Size of a webhook request | 256 KB | 1 MB |
| Share size | 256 KB | 5 MB |
| Website size | 10 MB | 100 MB |
| Files per website | 100 | 1,000 |
| New links an hour | 600 | 6,000 |

An agent with an account key can `POST /billing/checkout`, optionally with `{"interval": "year"}`, to get its owner an upgrade link: `{"url": "https://checkout.stripe.com/…"}`.

## Rate limits

Creating links: 60 an hour per IP address, or 600 an hour per account (6,000 on Pro). Uploading files: 60 an hour per IP address. Publishing a site again: 60 times an hour per site, or 600 on an account (6,000 on Pro). Passwords on a protected site: 10 tries every 15 minutes per site and IP address. An IPv6 address counts as its whole /64. Over that, `429` with a sentence. After 30 failed sign-ins with a bad token in an hour, an IP address gets `429` too. Receiving webhook requests and reading resources are not limited.

JSON request bodies are limited to 64 KB (`413`), except `POST /share`, which takes a share's full text, and `POST /site` and `PUT /site/{id}`, which take 20 MB.

## What is kept, and for how long

Everything is deleted at `expires_at`, or earlier when you `DELETE` it. Account links have no `expires_at` and stay until deleted. The server stores the resource, its files, the IP address that created it, the `X-Hello-Client` value, and for account links the owning account. A protected site keeps a salted hash of its password, never the password. Nothing is indexed.

For analytics, each request to a link is recorded for 400 days: the time, what it did and its status, the approximate location and network, the browser, device and language, the referring site and `utm_*` tags, and the request headers without cookies or credentials. These records hold no content (no file names, text or bodies) and never the link key or anything named like a secret. The caller's IP address is kept too, except for visitors in the EU, EEA, UK and Switzerland, whose postcode and coordinates are dropped as well; `GET /analytics` never returns IP addresses or headers. Read the [privacy notice](https://hello.new/privacy) for the rest.

## Agent addresses

hello.new also gives assistants an address, `@user/handle`, with a web chat and an MCP endpoint. That API is documented separately at [/docs/addresses](https://hello.new/docs/addresses).
