A JSON bin is a blob of JSON behind a URL your app can GET with its API key. No service to
deploy, no bucket to configure, no CORS to think about.
It exists for the config that is too dynamic to bake into a build and too small to justify a backend: feature flags, a pricing table, the copy on a landing page, a list of supported countries, remote kill switches, sample data for a demo.
https://api.apiverve.com/v1/jsonbin/<id>
| Plan | JSON bins |
|---|---|
| Free | 1 |
| Starter | 10 |
| Pro | 50 |
| Mega | 100 |
Static and dynamic
Two kinds, and the difference is whether anything can write to it after creation.
Static bins are read-only over the API. You edit the content in the dashboard; your app
reads it with GET. Available on every plan.
Dynamic bins are readable and writable — your code can PUT new content to the same URL.
Available on Pro and above.
The dashboard shows which is which on every bin, and the account panel tells you which you can create: dynamic bins on, or static bins only.
Choose static unless something other than a person needs to change the content. A config file a human edits twice a month is a static bin, and making it writable only widens what a leaked key can do. The type is fixed on the bin, not on your plan, so a static bin stays static even on Mega.
Creating one
VerveKit → JSON bins in the dashboard. Name it, choose the type, paste the JSON, save. The bin's URL appears on its card with a copy button, and the id in that URL is the only address the bin has.
Bins are created and deleted in the dashboard only. The API deliberately refuses both — a POST
or DELETE comes back 403 telling you to create bins via the APIVerve Dashboard — so a leaked
key cannot destroy your configuration or fill your account with bins.
About the id
A bin id is a fixed 28 characters: sixteen identifying the account, twelve identifying the bin.
Anything that is not exactly 28 characters is rejected as JSON bin not found before any lookup
happens, which is why a truncated copy-paste produces a 404 rather than a more helpful error.
The practical consequence: the id is the whole address. There is no separate ownership check on read, so treat a bin id the way you would treat the URL of an unlisted document — it is protected by needing a valid API key, and by not being guessable, and by nothing else.
Reading a bin
Same authentication as everything else:
curl 'https://api.apiverve.com/v1/jsonbin/YOUR_BIN_ID' \
-H 'x-api-key: YOUR_API_KEY'Reading a bin returns your JSON exactly as you stored it — no status, no error, no data
wrapper. That is deliberate: a config file should be usable as a config file, so
fetch(...).then(r => r.json()) hands you the object you saved.
It does mean the shared response handler you use for every other endpoint does not apply here.
A bin read is the one call where checking body.status will find nothing. See
response format.
Every other jsonbin operation — listing, updating — does use the envelope. Only the content read is raw, because only the content read is meant to be consumed by something that does not know about this API.
Responses carry open CORS headers, so a browser can read a bin directly. That is useful for a static site pulling its own config, and it is worth being clear-eyed about: doing it puts your API key in the page. See CORS and security.
A read costs one credit. That is the number that decides how you use bins: a config read once at boot is free in practice, and a config read on every request to your own app is a credit per request. Cache it — an hour, a deploy, whatever matches how often it actually changes — and the cost stops mattering. See making requests.
Listing your bins
GET /v1/jsonbin/list
Returns every bin on the account with its id, name, type, size, created and modified timestamps and its URL, most recently modified first. This one is enveloped, and it is scoped to the account the key belongs to. Useful for tooling; the dashboard is easier for everything else.
Writing to a dynamic bin
curl -X PUT 'https://api.apiverve.com/v1/jsonbin/YOUR_BIN_ID' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'content-type: application/json' \
-d '{"maintenance": true, "message": "Back at 14:00 UTC"}'The body replaces the bin's content entirely — there is no merge or patch. Read, modify, write the whole object if you need to change one field.
There is also no locking or compare-and-set. Two writers racing on the same bin means the last one wins and the other's change is gone without an error. Bins are for configuration with one writer; if several things write to the same document, that is the point at which you want a real datastore.
Four things are refused, each with a status worth handling:
| Writing to a static bin | 403 — This bin is static. Only dynamic bins can be updated |
| Writing on a plan without dynamic bins | 403 — Dynamic bin updates require Pro or Mega plan |
| A body that is not valid JSON | 400 — Invalid JSON |
| Content over your plan's response-size limit | 413, naming the actual size and the limit |
The plan check is applied on every write, not only at creation, so a dynamic bin on an account that drops to free becomes effectively read-only — the bin is unchanged and still readable, but the writes start failing. Worth knowing before you go looking for the fault in your own code.
That size ceiling is the same max response size your plan sets for API responses:
| Plan | Max response |
|---|---|
| Free | 100 KB |
| Starter | 5,000 KB |
| Pro | 5,000 KB |
| Mega | 5,000 KB |
It is a sensible bound to design to anyway. A bin is configuration, not a database — if you are approaching the ceiling, the thing you have built probably wants to be a real datastore.
A pattern worth copying
The version-and-cache shape covers most real uses:
let cache = null, fetchedAt = 0;
export async function config() {
if (cache && Date.now() - fetchedAt < 3_600_000) return cache;
const res = await fetch(`https://api.apiverve.com/v1/jsonbin/${BIN_ID}`, {
headers: { 'x-api-key': process.env.APIVERVE_API_KEY },
});
if (!res.ok) return cache ?? DEFAULTS; // never let config fetch break the app
cache = await res.json();
fetchedAt = Date.now();
return cache;
}Two things are doing the work. The hour-long cache turns a per-request credit into a per-instance one. The fall back to the last good value, then to a compiled-in default means a failed read degrades rather than breaks — which matters more for a kill switch than for anything else, since the moment you need it is the moment things are already going wrong.
Where bins earn their place
Remote configuration. Feature flags, thresholds, maintenance banners. Change the bin, and every client picks it up on its next read without a deploy.
Content a non-developer edits. The dashboard is a text box. Someone who is not going to open a pull request can still change the FAQ your app renders.
Demo and prototype data. A realistic payload behind a real URL, with no backend, which is usually enough to build a front end against.
A kill switch. A dynamic bin your operations tooling writes to, and your app reads, is the cheapest circuit breaker there is.
Coordination between workflow steps. A Zapier or n8n run has nowhere to keep state between executions; a bin gives it one, and the n8n node can read and update bins directly.
Where they are the wrong tool: anything per-user, anything you query rather than fetch whole, anything that changes many times a minute, anything with concurrent writers, and anything secret. A bin is protected by an API key, which means everything holding that key can read it — see key scoping if you need to narrow that.
Next
Mock endpoints are the same idea for a whole fake API rather than one document. Making requests covers the caching that keeps a bin's per-read cost irrelevant, and security covers where the key that reads it should live.