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"}
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
/v1/binsNone · Saves the body; returns url and edit_token. With an API key, the bin goes straight into your account.{url}None · Returns the document, raw./v1/bins/{id}Edit token or API key · Replaces the document. Same URL./v1/bins/{id}Edit token or API key · Deletes the bin; the URL returns 404./v1/bins/{id}/claimAPI key · Makes a trial bin permanent. Same URL./v1/bins/{id}Edit token or API key · The bin’s details: version, etag, size, expiry./v1/bins/{id}Edit token or API key · Renames the bin: {"name": "…"}./v1/bins/{id}/contentEdit token or API key · The document, fresh from storage. The only way to read a private bin./v1/bins/{id}/visibilityAPI key · {"visibility": "private"} or "public". Private needs Pro./v1/binsAPI key · Lists your bins, most recently updated first. ?limit= up to 100, then pass next_cursor as ?cursor=./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 gets401 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
- 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 answersBIN_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.
{ "error": { "code": "INVALID_JSON", "message": "Body isn't valid JSON: unexpected } at position 17. Remove the trailing comma before it." }, "request_id": "req_…" }
{ "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_…" }
{ "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_…" }
{ "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
INVALID_JSONThe body is empty, not UTF-8, or not JSON. The message says where.INVALID_REQUESTA bad query parameter, e.g. limit or cursor.AUTH_REQUIREDNo credentials. Add Authorization: Bearer ….AUTH_INVALIDThe key or token isn’t recognised. Check for copy-paste errors; a claimed bin’s edit token stops working.AUTH_REVOKEDThe API key was revoked. Create a new one in the dashboard.FORBIDDENThe credentials can’t do this: an edit token on another bin, an API key without the scope, or a suspended account.VISIBILITY_NOT_ALLOWEDPrivate bins need Pro, which is invite-only for now.BIN_LIMIT_REACHEDYour account has as many bins as its plan allows. Delete some first.STORAGE_LIMIT_REACHEDYour bins use all of your plan’s storage. Shrink or delete some.API_KEY_LIMIT_REACHEDRevoke a key before creating another.BIN_SUSPENDEDA moderator took the bin offline. Contact support from the dashboard.BIN_NOT_FOUNDNo such bin, or it isn’t yours. Bins you can’t see always look like this.BIN_EXPIREDThe trial bin passed expires_at. It can’t be claimed any more.ETAG_MISMATCHThe bin changed since the If-Match etag. Re-read and retry.PAYLOAD_TOO_LARGEThe body is over your size limit. The message gives both sizes.INVALID_METADATAA JSON request field is wrong, e.g. a name over 100 characters.RATE_LIMITEDToo many requests this minute. Wait for Retry-After seconds.WRITE_LIMIT_REACHEDA daily limit is used up. It resets at 00:00 UTC.INTERNAL_ERROROur fault. Retry once; if it persists, send us the request_id.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.