Get up and running

Quick start

Make your first API call in under 2 minutes. No SDK needed — just fetch.

1
Get your public key

Go to Dashboard → select your site → API. Your public key (pnd_pub_...) is already generated. Copy it.

2
Find your site ID

It's in the URL when you're in the dashboard: dashboard/cms?site=YOUR_SITE_ID

3
Make your first request
JavaScript — fetch your content
const SITE_ID  = 'YOUR_SITE_ID';
const PUB_KEY  = 'pnd_pub_YOUR_KEY';
const BASE_URL = 'https://api.foundiq.nl/v1';

function headers() {
  return { 'Authorization': `Bearer $${PUB_KEY}` };
}

// 1. Fetch all collections
const res = await fetch(`$${BASE_URL}/sites/$${SITE_ID}/collections`, { headers: headers() });
const { data: collections } = await res.json();

// 2. Fetch entries in a collection
const res2 = await fetch(`$${BASE_URL}/sites/$${SITE_ID}/collections/products/entries`, { headers: headers() });
const { data: products, meta } = await res2.json();

console.log(`Loaded $${products.length} of $${meta.total} products`);
You'll get back
{
  "ok": true,
  "data": [
    { "id": "...", "status": "published", "data": { "name": "Air Max 90", "price": 120 } },
    ...
  ],
  "meta": { "total": 42, "limit": 20, "offset": 0, "has_more": true }
}
What next?
v1

Introduction

The Foundiq delivery API lets you fetch your content from any frontend — Next.js, SvelteKit, vanilla JS, mobile apps, anywhere that can make an HTTP request. It's read-optimised, CORS-enabled, and cacheable by default.

🔑
Two key types
Public keys for browsers. Secret keys for servers.
Cached responses
60s cache + stale-while-revalidate for fast delivery.
🌍
CORS enabled
Call from any domain — no proxy needed.

Authentication

Every request requires a Bearer token in the Authorization header. Foundiq issues two types of token per site — choose the right one for your context.

Key typePrefixWhere to usePermissions
Publicpnd_pub_Browser JS, mobile apps, public frontendsRead published content, decrement stock
Secretpnd_sec_Server-side only — never in client codeAll public permissions + write entries
Authorization header
Authorization: Bearer pnd_pub_your_key_here
⚠ Never expose your secret key in client-side code. If compromised, rotate it immediately from Dashboard → API.

Base URL

All API requests are made to:

Base URL
https://api.foundiq.nl/v1

Collections

Collections are the content types you define in the CMS — products, blog posts, FAQs etc. Returns all collections with published entry counts.

GET /v1/sites/:siteId/collections Public
Request
fetch('https://api.foundiq.nl/v1/sites/SITE_ID/collections', {
  headers: { 'Authorization': 'Bearer pnd_pub_YOUR_KEY' }
})
Response 200
{
  "ok": true,
  "data": [
    {
      "id": "bbc36e1f-...",  "name": "products",
      "label": "Products",  "color": "#00d4ff",
      "fields": [...],  "entry_count": 42
    }
  ]
}

List entries

Returns published entries in a collection. Supports filtering, search, and pagination. Drafts are never returned.

GET /v1/sites/:siteId/collections/:name/entries Public
Query parameters
ParameterTypeDefaultDescription
limitinteger20Max entries. Maximum 100.
offsetinteger0Entries to skip.
orderstringcreated_atcreated_at or updated_at
dirstringdescasc or desc
searchstringFull-text search across all data fields.
[field]stringFilter by any data field. e.g. ?category=shoes
Request
// Fetch 10 products in the "shoes" category
fetch('https://api.foundiq.nl/v1/sites/SITE_ID/collections/products/entries?limit=10&category=shoes', {
  headers: { 'Authorization': 'Bearer pnd_pub_YOUR_KEY' }
})
Response 200
{
  "ok": true,
  "data": [
    {
      "id": "191b2302-...",  "status": "published",
      "data": { "name": "Air Max 90", "price": 120, "stock": 8 },
      "created_at": "2025-05-01T10:00:00Z"
    }
  ],
  "meta": { "total": 42, "limit": 10, "offset": 0, "has_more": true }
}

Single entry

Returns a single published entry by ID.

GET /v1/sites/:siteId/collections/:name/entries/:entryId Public
Request
fetch('https://api.foundiq.nl/v1/sites/SITE_ID/collections/products/entries/ENTRY_ID', {
  headers: { 'Authorization': 'Bearer pnd_pub_YOUR_KEY' }
})

Create entry

Write an entry from an external system — contact forms, newsletter signups, order records, reviews, or any structured data you want in the CMS. Requires a secret key. Collection is auto-created if it doesn't exist.

POST /v1/sites/:siteId/collections/:name/entries Secret
Request body
FieldTypeRequiredDescription
dataobjectYour entry data — any JSON object.
statusstring"published" (default) or "draft"
Contact form — server-side handler
await fetch('https://api.foundiq.nl/v1/sites/SITE_ID/collections/contact/entries', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pnd_sec_YOUR_SECRET_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    data: {
      name:    'Ada Lovelace',
      email:   'ada@example.com',
      message: 'Hello, I have a question...',
    }
  })
})
The submission instantly appears in your CMS dashboard under the contact collection.

Decrement stock

Atomically decrements a numeric field — typically stock. Call this from your payment webhook after a successful checkout. Idempotent — passing the same order_id twice never double-decrements.

POST /v1/sites/:siteId/collections/:name/entries/:entryId/decrement Public
Request body
FieldTypeRequiredDescription
order_idstringYour payment provider's charge ID. Used for idempotency.
quantityintegerHow much to decrement. Default: 1.
fieldstringWhich field to decrement. Default: "stock".
order_dataobjectExtra data stored on the order record.
Stripe webhook handler
const charge = event.data.object;

const res = await fetch(
  `https://api.foundiq.nl/v1/sites/$${SITE_ID}/collections/products/entries/$${productId}/decrement`,
  {
    method: 'POST',
    headers: { 'Authorization': `Bearer $${PUB_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      order_id:   charge.id,
      quantity:   1,
      order_data: { customer_email: charge.billing_details.email, amount: charge.amount }
    })
  }
);

const { ok, remaining, code } = await res.json();
if (!ok && code === 'OUT_OF_STOCK') { /* refund */ }
Response 200 — success
{ "ok": true, "remaining": 7 }
Response 409 — out of stock
{ "ok": false, "code": "OUT_OF_STOCK", "remaining": 0 }
Every successful decrement creates an order record in an _orders collection visible in your CMS dashboard.

List media files

Returns media files uploaded to a site. Filter by MIME type prefix.

GET /v1/sites/:siteId/media Public
Query parameters
ParameterDescription
mime_typeFilter by MIME type. image/ matches all images.
nameFilter by exact filename.
limit, offsetPagination.
Fetch all images
fetch('https://api.foundiq.nl/v1/sites/SITE_ID/media?mime_type=image/', {
  headers: { 'Authorization': 'Bearer pnd_pub_YOUR_KEY' }
})

Single media file

Fetch a single media file by its filename.

GET /v1/sites/:siteId/media/:filename Public
Request
fetch('https://api.foundiq.nl/v1/sites/SITE_ID/media/hero-banner.jpg', {
  headers: { 'Authorization': 'Bearer pnd_pub_YOUR_KEY' }
})

Filtering & search

Filter entries by any field in their data object. Multiple filters combine with AND.

Field filter
// ?category=shoes&brand=nike → data.category = 'shoes' AND data.brand = 'nike'
GET /v1/sites/SITE_ID/collections/products/entries?category=shoes&brand=nike
Full-text search
// Searches across ALL fields in data
GET /v1/sites/SITE_ID/collections/products/entries?search=running+shoe

Pagination

All list endpoints return a meta object. Use has_more to know when to stop.

Fetch all entries
async function fetchAll(siteId, collection, token) {
  const results = [];
  let offset = 0;
  const limit  = 100;

  while (true) {
    const res = await fetch(
      `https://api.foundiq.nl/v1/sites/$${siteId}/collections/$${collection}/entries?limit=$${limit}&offset=$${offset}`,
      { headers: { 'Authorization': `Bearer $${token}` } }
    );
    const { data, meta } = await res.json();
    results.push(...data);
    if (!meta.has_more) break;
    offset += limit;
  }
  return results;
}

Error reference

All errors return a consistent shape with a code you can match on.

Error shape
{ "ok": false, "code": "NOT_FOUND", "message": "Collection \"products\" not found." }
HTTPCodeDescription
401MISSING_TOKENNo Authorization header.
401INVALID_TOKENToken invalid or revoked.
403FORBIDDENToken has no access to this site.
403SECRET_KEY_REQUIREDEndpoint requires a secret key.
404NOT_FOUNDCollection, entry, or file not found.
409OUT_OF_STOCKInsufficient stock for decrement.
200ALREADY_PROCESSEDorder_id already decremented.