Docs/Platform/JSON bins

JSON bins

Serve JSON from a key-protected URL — static or writable — without standing up a service for it.

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.

code
https://api.apiverve.com/v1/jsonbin/<id>
PlanJSON bins
Free1
Starter10
Pro50
Mega100

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:

bash
curl 'https://api.apiverve.com/v1/jsonbin/YOUR_BIN_ID' \
  -H 'x-api-key: YOUR_API_KEY'
A bin read is not wrapped in the envelope

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

code
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

bash
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 bin403This bin is static. Only dynamic bins can be updated
Writing on a plan without dynamic bins403Dynamic bin updates require Pro or Mega plan
A body that is not valid JSON400Invalid JSON
Content over your plan's response-size limit413, 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:

PlanMax response
Free100 KB
Starter5,000 KB
Pro5,000 KB
Mega5,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:

js
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.

Was this page helpful?

Last updated