Docs
Nibbleful stores JSON records in Collections and bills prepaid credit per accepted API request. Rejected requests are free. Dashboard browsing and exports are free.
Concepts
Organization
The billing account. Prepaid balance, team members, and API keys belong to the organization.
Collection
A JSON data store with a stable public UUID. Create one per form, inbox, or product surface.
Record
One stored JSON object. The body must be an object, not an array.
Keys starting with _nibbleful_ are reserved and not stored.
Prepaid balance
There are no monthly plans. Accepted API operations debit the organization balance.
A 402 means the organization does not have enough credit yet.
First request
- 1. Add prepaid funds. Top up from Billing. The first accepted write costs $0.001 plus transfer if the request exceeds 1 MB.
-
2. Create a Collection.
Copy its public UUID. Leave it active — a paused collection rejects writes with
403. -
3. Pick an API.
Use a private
nib_sk_*key from your server to read and write. Use a publicnib_pk_*token only for browser write-only ingest. -
4. POST a JSON object.
A successful create returns
201with the stored record. CopyX-Nibbleful-Request-Idif anything fails — it matches a row in Activity.
Private API
Server-side only. Never put a nib_sk_* key in frontend code.
Keys are granted per collection. A key with no collection permissions cannot read or write —
the API returns collection_not_found.
Permissions cannot be edited later; revoke the key and create a new one.
The plaintext secret is shown once at creation.
Endpoints
POST https://nibbleful.com/api/v1/collections/:collection_id/records GET https://nibbleful.com/api/v1/collections/:collection_id/records GET https://nibbleful.com/api/v1/collections/:collection_id/records/:record_id
Auth
Authorization: Bearer nib_sk_test_...
WRITE is required to POST. READ is required to GET. Test tokens start with
nib_sk_test_; production tokens start with
nib_sk_live_.
Create
curl -X POST https://nibbleful.com/api/v1/collections/COLLECTION_ID/records \
-H "Authorization: Bearer nib_sk_..." \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","source":"website"}'
Success body
{
"id": "01j...",
"created_at": "2026-09-17T20:00:00.000Z",
"data": { "email": "[email protected]", "source": "website" }
}
List
Newest first. limit defaults to 50 and maxes at 100.
Pass a record id as before or after to page.
{
"data": [ { "id": "...", "created_at": "...", "data": {} } ],
"pagination": { "next": "01j...", "previous": null }
}
Public ingest
Write-only ingest from the browser. Enable it on the collection, then send
X-Nibbleful-Token with the collection’s
nib_pk_* token.
The token is not a secret and is not authentication by itself. It cannot read, query, or delete data.
Endpoint
POST https://nibbleful.com/public/v1/collections/:collection_id/records
CORS allows POST and OPTIONS.
Allowed request headers: Content-Type,
Accept,
X-Nibbleful-Token,
X-Nibbleful-Turnstile-Token.
Browser example
const response = await fetch(
"https://nibbleful.com/public/v1/collections/COLLECTION_ID/records",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Nibbleful-Token": "nib_pk_test_..."
},
body: JSON.stringify({
email: "[email protected]"
})
}
);
Public ingest checks run in this order: collection exists, ingest enabled, token valid, not paused, payload size, JSON object, origin, rate limit, Turnstile, honeypot, duplicate, daily spend, then balance. A later failure can still happen after an earlier check has already recorded state — see duplicate protection below.
Collection states
| State | API behavior |
|---|---|
| Active | Reads and writes are accepted when auth, scope, and balance allow it. |
| Paused |
Private reads/writes and public ingest return 403 collection_paused.
Records stay. Resume from Settings.
|
| Archived / deleted |
Returns 404 collection_not_found.
Stored payloads are erased. Billing history is kept.
|
Public ingest protections
These apply only to /public/v1. Rejected requests are free.
Configure them on the collection Settings tab.
| Control | Default | What it does |
|---|---|---|
| Allowed origins | Empty | If set, the request Origin must match exactly. Origin is not authentication and can be spoofed outside a browser. |
| Turnstile | Off | Requires a Cloudflare Turnstile widget token in X-Nibbleful-Turnstile-Token. |
| Honeypot | Off |
If a client sends a non-empty _nibbleful_hp field, the request is rejected.
Keep that field empty in real forms.
|
| Duplicate protection | On |
Fingerprints collection + JSON body + client IP for 60 seconds and returns
409 duplicate on repeats.
The fingerprint is claimed when the check runs, even if a later check such as balance fails.
Wait, change the body, or turn this off to retry the same payload.
|
| Rate limits | Always on | 10 requests per IP per collection per 60 seconds, and 60 requests per collection per 60 seconds. |
| Daily spend limit | Optional | Caps public ingest charges for the collection. Private API writes are not affected. |
Pricing
- Accepted API requests cost $0.001.
- The first 1 MB of transfer (bytes in + bytes out) is included per accepted request. Each started additional MB is $0.001.
- Storage is $1/GB-month based on retained byte-time, billed monthly per collection, rounded down to $0.001.
- Rejected requests, dashboard browsing, and exports are not billed as API usage.
- Top-ups are $5–$100. Optional auto top-up charges a saved card when balance is low; the credit appears after Stripe confirms.
Errors
Failures return JSON. Read error.code, not only the HTTP status —
several protections share 403.
{
"error": {
"code": "collection_paused",
"message": "This collection is paused."
}
}
| HTTP | Code | API | What to do |
|---|---|---|---|
| 401 |
missing_api_key
The API key is missing. |
Private | Send Authorization: Bearer nib_sk_... on private API requests. |
| 401 |
invalid_api_key
The supplied API key is invalid. |
Private | The secret does not match any key. Secrets are shown only once at creation. |
| 401 |
revoked_api_key
The supplied API key has been revoked. |
Private | Create a new key. Revoked keys cannot be restored. |
| 404 |
collection_not_found
The collection was not found or this API key does not have access to it. |
Both | The collection UUID is wrong, the collection is archived, or this private key is not granted access. The API does not distinguish those cases. |
| 403 |
insufficient_scope
The API key does not have permission for this operation. |
Private | The key exists but lacks READ or WRITE on this collection. Revoke it and create a new key with the right permissions. |
| 403 |
collection_paused
This collection is paused. |
Both | Resume the collection on its Settings tab. Pause rejects private reads/writes and public ingest. Existing records stay in place. |
| 400 |
invalid_payload
The request body must be a JSON object. |
Both | The body must be a JSON object, for example {"email":"[email protected]"}. Arrays, empty bodies, and invalid JSON are rejected. |
| 413 |
payload_too_large
The request payload exceeds the 25 MB limit. |
Both | Request bodies are capped at 25 MB. |
| 404 |
record_not_found
The record was not found. |
Private | The record UUID is wrong, or the record was deleted. |
| 402 |
insufficient_balance
The organization does not have enough prepaid balance. |
Both | Top up prepaid credit before retrying. Rejected requests are free. Public ingest may attempt auto top-up if it is enabled, but the credit lands only after Stripe confirms, so this request still returns 402. |
| 403 |
public_ingest_disabled
Public ingest is not enabled for this collection. |
Public | Enable public ingest on the collection. A public token is required. |
| 401 |
invalid_public_token
The supplied public ingest token is invalid. |
Public | Use the current nib_pk_* token in X-Nibbleful-Token. Rotating the token invalidates the previous one. |
| 403 |
origin_rejected
The request origin is not allowed. |
Public | The Origin header is not on the collection allowlist. Leave the allowlist empty to skip origin checks. |
| 429 |
rate_limited
Too many requests. Try again later. |
Public | Public ingest allows 10 requests per IP per collection per 60 seconds, and 60 requests per collection per 60 seconds. Retry-After is set on 429 responses. |
| 403 |
turnstile_missing
A Turnstile token is required. |
Public | This collection requires Cloudflare Turnstile. Send the widget token in X-Nibbleful-Turnstile-Token. |
| 403 |
turnstile_failed
The request could not be verified. |
Public | The Turnstile token was missing, invalid, or could not be verified. Get a fresh widget token and retry. |
| 403 |
honeypot
The request was rejected. |
Public | A reserved honeypot field was filled in. Keep _nibbleful_hp empty in real browser forms. |
| 409 |
duplicate
This request was already received. |
Public | The same JSON body was already seen from this IP for this collection in the last 60 seconds. The fingerprint is claimed when that check runs, even if a later check such as insufficient balance rejects the request. Wait, change the payload, or disable duplicate protection. |
| 403 |
daily_spend_limit_reached
This collection has reached its daily spend limit. |
Public | This collection's public ingest daily spend cap was reached. Private nib_sk_* writes are not affected. |
| 500 |
internal_error
An internal error occurred. |
Both | The request was not accepted. Check Activity for the request id in X-Nibbleful-Request-Id. |
Debugging
- Every API response includes
X-Nibbleful-Request-Id. Open Activity and search that id. - Activity shows outcome, rejection reason, and public ingest protection results (token, origin, Turnstile, honeypot, duplicate, spend cap).
- If the browser shows
403, check whether the collection is paused before assuming the public token is wrong. - If you see
402then409on the same body, add funds and wait 60 seconds or change the payload. Duplicate protection already claimed the first attempt. - Dashboard record lists and CSV/JSON exports do not consume API credit.