Quickstart

Save a JSON document, get a URL, fetch it from anywhere. Free for hobby projects.

$ curl -d '{"hello":"world"}' https://api.jstash.app/v1/bins
{
  "url": "https://j.jstash.app/LmP6w8kC2fY7Qv4rZx1aNg",
  "edit_token": "js_edit_…",
  "expires_at": "2026-10-09T10:30:00.000Z",
  "keep_it": "Sign in at https://app.jstash.app to make this bin permanent.",
  "id": "LmP6w8kC2fY7Qv4rZx1aNg",
  "name": null,
  "visibility": "public",
  "api_url": "https://api.jstash.app/v1/bins/LmP6w8kC2fY7Qv4rZx1aNg/content",
  "bytes": 17,
  "sha256": "93a239…",
  "version": 1,
  "etag": "\"fbc24bcc7a1794758fc1327fcfebdaf6\"",
  "created_at": "2026-10-02T10:30:00.000Z",
  "updated_at": "2026-10-02T10:30:00.000Z"
}
const data = await fetch("https://j.jstash.app/LmP6w8kC2fY7Qv4rZx1aNg").then(r => r.json());
// {"hello":"world"}
Use the url exactly as returned. Writes go to api.jstash.app and reads come from j.jstash.app — you never have to put that together yourself.

Save the edit_token. It is shown once and is the only way to change or delete a trial bin. jstash stores only a fingerprint of it, so a lost token can’t be recovered.

The body can be any JSON value — an object, an array, a string, a number, true, false or null — as UTF-8 text. It’s stored byte for byte, so your formatting and key order come back exactly as sent.

Authorization

There is one header. A bin’s edit token and your account’s API key both go in it. Reading a public bin or creating a trial bin needs neither.

Authorization: Bearer js_edit_…   # updates or deletes one bin
Authorization: Bearer js_live_…   # your account’s API key

An edit token works only on its own bin: GET, PUT, PATCH and DELETE on /v1/bins/{id}, plus reading /v1/bins/{id}/content. An API key works on every bin in your account.

Call the API from a server, a script or CI. Browser JavaScript on other websites can’t call api.jstash.app, which keeps API keys and edit tokens out of public pages. Reading bins from the browser is fine — see Reading a bin. To let each signed-in user of your web app save their own data from the browser, use jstash Apps.

Endpoints

POST/v1/binsNone · Saves the body; returns url and edit_token. With an API key, the bin goes straight into your account.
GET{url}None · Returns the document, raw.
PUT/v1/bins/{id}Edit token or API key · Replaces the document. Same URL.
DELETE/v1/bins/{id}Edit token or API key · Deletes the bin; the URL returns 404.
POST/v1/bins/{id}/claimAPI key · Makes a trial bin permanent. Same URL.
GET/v1/bins/{id}Edit token or API key · The bin’s details: version, etag, size, expiry.
PATCH/v1/bins/{id}Edit token or API key · Renames the bin: {"name": "…"}.
GET/v1/bins/{id}/contentEdit token or API key · The document, fresh from storage. The only way to read a private bin.
POST/v1/bins/{id}/visibilityAPI key · {"visibility": "private"} or "public". Private needs Pro.
GET/v1/binsAPI key · Lists your bins, most recently updated first. ?limit= up to 100, then pass next_cursor as ?cursor=.
GET/v1/account/usageAPI key · Bins, storage, today’s writes and API keys against your limits.

Any Content-Type is accepted — curl -d sends form-encoded and that’s fine. The body only has to parse as JSON. To send a file, use --data-binary @file.json; plain -d @file strips newlines.

curl -X PUT https://api.jstash.app/v1/bins/LmP6w8kC2fY7Qv4rZx1aNg \
  -H "Authorization: Bearer js_edit_…" \
  -d '{"hello":"again"}'

PUT returns {"id", "version", "bytes", "etag", "updated_at"}. DELETE returns 204 and is safe to repeat.

Making a bin permanent

Sign in at app.jstash.app, create an API key, then claim the bin with its edit token. It stops expiring and keeps its URL. You can also paste the URL and token into Claim a trial bin in the dashboard.

curl -X POST https://api.jstash.app/v1/bins/LmP6w8kC2fY7Qv4rZx1aNg/claim \
  -H "Authorization: Bearer js_live_…" \
  -d '{"edit_token":"js_edit_…"}'

Claim before expires_at. The bin counts toward your account’s limits, and once it’s claimed its edit token stops working — use your API key from then on.

Reading a bin

The url works from browsers, servers and shells with a plain GET (or HEAD). It returns your document with Content-Type: application/json, an ETag, and Access-Control-Allow-Origin: *, so any website can fetch() it.

  • Changes show up on the next read. Reads are cached at Cloudflare’s edge, and every update or delete clears that cache. If a purge ever fails, the cached copy expires within 5 minutes.
  • Don’t add query strings to bust the cache. They’re ignored, and you don’t need them.
  • Keep browser requests simple. Don’t send custom headers or credentials to j.jstash.app — the bin doesn’t need them, and they make the browser send a CORS preflight that isn’t supported.
  • Revalidation is free. Browsers store the bin and revalidate it with If-None-Match.

Private bins are part of Pro, which is invite-only for now. They have no public URL: read them from /v1/bins/{id}/content with your API key. Create one with the header X-Bin-Visibility: private, or switch an existing bin with the Private switch in the dashboard or POST /v1/bins/{id}/visibility and {"visibility": "private"}. Switching to private stops the public URL straight away.

Updating safely

A PUT replaces the whole document; there are no partial updates. If more than one thing writes the same bin, send the bin’s current etag in If-Match so you never overwrite a change you haven’t seen:

curl -X PUT https://api.jstash.app/v1/bins/LmP6w8kC2fY7Qv4rZx1aNg \
  -H "Authorization: Bearer js_edit_…" \
  -H 'If-Match: "fbc24bcc7a1794758fc1327fcfebdaf6"' \
  -d '{"hello":"again"}'

If the bin changed since you read that etag, you get 412 ETAG_MISMATCH and nothing is written. Fetch the latest version, merge, and try again. Every save returns the new etag and version.

Saving a document identical to the current one is a no-op: no new version, and it doesn’t count toward your daily updates.

Bin details and names

GET /v1/bins/{id} returns the same fields as the create response (without the edit token). Use it to check a trial bin’s expires_at or the current etag.

curl -X PATCH https://api.jstash.app/v1/bins/LmP6w8kC2fY7Qv4rZx1aNg \
  -H "Authorization: Bearer js_edit_…" \
  -d '{"name":"Plant tracker config"}'

Names are 1–100 characters, or null to clear. They’re private labels for you and the dashboard; they never change the URL.

Accounts and API keys

Sign in at app.jstash.app with GitHub. Signing in the first time creates your account. From the dashboard you can create and edit bins, claim trial bins, see your usage, and manage API keys.

  • API keys start with js_live_, are shown once when created, and can be revoked at any time (anything using a revoked key gets 401 AUTH_REVOKED). To rotate a key, create the new one, switch over, then revoke the old one.
  • Bins you create with an API key belong to your account straight away: no edit token, no expiry.
  • Protect your GitHub account. It’s how you sign in to the dashboard, and the dashboard can create API keys for every bin you own — turn on GitHub two-factor authentication.
  • Your profile holds your name and an optional email we use only to reply to you and to announce Pro.
  • Deleting your account (Profile → Delete account) revokes every key and session, removes your GitHub link and personal details, and deletes every bin. Bin URLs stop working within two hours. It can’t be undone.

Limits

Trial binsLimit
Size100 KB
VisibilityPublic — anyone with the URL can read it
Lifetime7 days, unless claimed
Creates per IP5 a minute, 20 a day
Updates per IP1,000 a day
Don’t store secrets in a trial bin. Its URL is the only thing between the document and the internet.
Free accountLimit
Size100 KB per bin
Bins100 bins, 10 MB in total
Creates100 a day
Updates1,000 a day
API keys2
Per API keyRate limit
Creates10 a minute
Updates and renames60 a minute
Deletes30 a minute
Reads and lists300 a minute
  • Daily limits reset at 00:00 UTC. Per-minute limits are rolling and approximate.
  • Sizes are the bytes you send. 1 KB is 1,000 bytes and 1 MB is 1,000,000 bytes.
  • “Per IP” counts IPv6 addresses by their /64 network, since most providers give each customer a whole /64.
  • Trial bins are deleted within about 15 minutes after expires_at. From that moment the API answers BIN_EXPIRED.
  • Public reads aren’t metered. A single IP sending more than about 60 requests a second is briefly blocked.
  • Pro (invite-only for now) limits are on the pricing page.

Errors

Every error is JSON with a stable code to branch on and a message that names the problem and the fix. Print the message — it’s written for you to read. The request_id finds your request in our logs if you ask for help.

400The body doesn’t parseINVALID_JSON
{ "error": { "code": "INVALID_JSON", "message": "Body isn't valid JSON: unexpected } at position 17. Remove the trailing comma before it." }, "request_id": "req_…" }
401An update or delete has no tokenAUTH_REQUIRED
{ "error": { "code": "AUTH_REQUIRED", "message": "Updating a bin needs its edit token. Add the header Authorization: Bearer js_edit_… — it came back when you created the bin." }, "request_id": "req_…" }
404A trial bin has expiredBIN_EXPIRED
{ "error": { "code": "BIN_EXPIRED", "message": "This bin expired on 2026-10-09. Trial bins last 7 days; claim one with POST /v1/bins/{id}/claim before then to keep it." }, "request_id": "req_…" }
429Too many creates from one IPRATE_LIMITED
{ "error": { "code": "RATE_LIMITED", "message": "That's 5 new bins this minute from your IP. Try again in a minute. The daily limit is 20." }, "request_id": "req_…" }

All error codes

Status · codeMeaning and what to do
400 INVALID_JSONThe body is empty, not UTF-8, or not JSON. The message says where.
400 INVALID_REQUESTA bad query parameter, e.g. limit or cursor.
401 AUTH_REQUIREDNo credentials. Add Authorization: Bearer ….
401 AUTH_INVALIDThe key or token isn’t recognised. Check for copy-paste errors; a claimed bin’s edit token stops working.
401 AUTH_REVOKEDThe API key was revoked. Create a new one in the dashboard.
403 FORBIDDENThe credentials can’t do this: an edit token on another bin, an API key without the scope, or a suspended account.
403 VISIBILITY_NOT_ALLOWEDPrivate bins need Pro, which is invite-only for now.
403 BIN_LIMIT_REACHEDYour account has as many bins as its plan allows. Delete some first.
403 STORAGE_LIMIT_REACHEDYour bins use all of your plan’s storage. Shrink or delete some.
403 API_KEY_LIMIT_REACHEDRevoke a key before creating another.
403 BIN_SUSPENDEDA moderator took the bin offline. Contact support from the dashboard.
404 BIN_NOT_FOUNDNo such bin, or it isn’t yours. Bins you can’t see always look like this.
404 BIN_EXPIREDThe trial bin passed expires_at. It can’t be claimed any more.
412 ETAG_MISMATCHThe bin changed since the If-Match etag. Re-read and retry.
413 PAYLOAD_TOO_LARGEThe body is over your size limit. The message gives both sizes.
422 INVALID_METADATAA JSON request field is wrong, e.g. a name over 100 characters.
429 RATE_LIMITEDToo many requests this minute. Wait for Retry-After seconds.
429 WRITE_LIMIT_REACHEDA daily limit is used up. It resets at 00:00 UTC.
500 INTERNAL_ERROROur fault. Retry once; if it persists, send us the request_id.
503 R2_UNAVAILABLE, DATABASE_UNAVAILABLEStorage is briefly unavailable. Retry with a short backoff; nothing was half-written.

Retrying: PUT and DELETE are safe to retry. POST /v1/bins is not — if a create times out and you retry, you may end up with two bins. Delete the extra one, or check your bin list before retrying.

Troubleshooting

I lost my edit token.

It can’t be recovered — jstash only keeps a fingerprint of it. A trial bin without its token can’t be changed, deleted or claimed, and it’s removed automatically when it expires. Make a new bin, keep the token, and claim the bin if you want it permanently.

I updated a bin but still see the old content.

Check the PUT response: its version should have gone up. Updates clear the cache straight away, so the next read of the url returns the new document. If you read through your own server or a CDN, its cache may be the stale one.

My browser shows a CORS error when reading a bin.

Use a plain fetch(url) without custom headers, credentials: 'include' or a non-GET method. Writes from browser JavaScript on your site aren’t supported; do them from a server or script.

I get 429 errors.

RATE_LIMITED means too many requests this minute; wait for the Retry-After seconds. WRITE_LIMIT_REACHED means a daily limit is used up; it resets at 00:00 UTC. Signing in raises the trial limits — see Limits.

My trial bin expired.

Expired bins are deleted and can’t be claimed. Next time, claim the bin before expires_at, or create it with an API key so it never expires.

My bin says it was taken offline.

A moderator suspended it after a report. If you think that’s a mistake, use Contact support in the dashboard (Profile → Support).

Help and privacy

  • Support: email support@jstash.app, or if you’re signed in, use Profile → Contact support in the dashboard. We reply by email.
  • Report a bin that hosts spam, malware, phishing or someone’s personal data at jstash.app/report.
  • Public means public. Bin IDs are random and unguessable, but anyone who has a URL can read the bin. Never store passwords, API keys or personal data in a public bin.
  • Backups: jstash doesn’t keep old versions. Replaced or deleted content can’t be recovered, so keep your own copy of anything important.
  • No SLA. jstash runs on a best-effort basis with no uptime guarantee, and doesn’t offer a data processing agreement yet. Don’t use it for data that needs either.
  • The fine print: jstash is run by Neonook Pty Ltd (ABN 80 124 800 853). See the Terms of Service and Privacy Policy.