Overview
Food Web is the system of record. Your site keeps its own pages, blog and submission queue; it reads organizations, programs and dates from the API on the server, and sends programs your coordinator approves back through the suggestions endpoint. A saved map names your member organizations, and a curator area decides what you may publish without Food Web review.
Server-side only
Setup
Food Web gives you an account with the partner and local-curator roles, a curator area, a saved-map token and a key with map:read and suggestions:write. Store the key and the token as server environment variables (FOODWEB_API_KEY, FOODWEB_SAVED_MAP).
// lib/foodweb.ts — server only
import 'server-only';
const BASE = process.env.FOODWEB_API_BASE ?? 'https://api.foodweb.network/v1';
export async function foodweb<T>(path: string, revalidate = 300): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${process.env.FOODWEB_API_KEY}` },
next: { revalidate }
});
if (res.status === 404) throw new Error('not_found');
if (!res.ok) throw new Error(`Food Web ${res.status}`);
return res.json() as Promise<T>;
}Directory
Your saved map resolves to its organizations. Read them with your key to get phone and email; anonymous reads leave those blank.
// app/directory/page.tsx — the member organizations on your saved map
import { foodweb } from '@/lib/foodweb';
export default async function Directory() {
// The maps embed endpoint resolves a public saved map to its organizations.
const res = await fetch(
`https://maps.foodweb.network/api/embed/entities?token=${process.env.FOODWEB_SAVED_MAP}`,
{ next: { revalidate: 300 } }
);
const { items } = await res.json();
// For phone and email, read each organization with your key:
const ids = items.map((i: { entity_id: string }) => i.entity_id).join(',');
const { items: orgs } = await foodweb<{ items: any[] }>(`/map/entities?ids=${ids}`);
return <ul>{orgs.map((o) => <li key={o.entity_id}>{o.name} · {o.phone}</li>)}</ul>;
}Programs
Filter by category, audience, access, cost, kind or frequency. Each program has a human schedule label, its next date, its organizer and a canonical page on Food Web. The programs guide lists every parameter.
const { items } = await foodweb<{ items: any[]; next_cursor: string | null }>(
`/programs?saved_map=${process.env.FOODWEB_SAVED_MAP}&categories=free-food,community-meal&limit=100`
);
// items[].schedule_label → "Every Wednesday · 1–3pm"
// items[].next_occurrence → { starts_at: "2026-11-18T13:00:00-04:00", … }
// items[].organizer → { id, name, logo_url, website, map_url }
const { program } = await foodweb<{ program: any }>(`/programs/${slug}`);Calendar
// A month grid: at most 93 days per call, in your timezone.
const { items } = await foodweb<{ items: any[] }>(
`/programs/occurrences?saved_map=${process.env.FOODWEB_SAVED_MAP}&from=2026-11-01&to=2026-12-01&tz=America/Halifax`
);
// Group by items[].starts_at.slice(0, 10). Strike through items[].status === 'cancelled'.
// items[].program.schedule_confirmed === false → "Dates are estimates until the organizer confirms."JSON-LD & ICS
Embed structured_data as-is so search engines see the same Event markup Food Web publishes, and set your own canonical on your page. Offer subscribe links with webcal://.
{program.structured_data && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(program.structured_data).replace(/</g, '\\u003c') }}
/>
)}
<a href={program.ics_url.replace('https://', 'webcal://')}>Subscribe in your calendar</a>Sending submissions
When your coordinator approves a submission in your queue, send it. Inside your curator area it publishes at once; outside it, or with auto_apply: false, a Food Web curator reviews it. Add dry_run: true to test a push without storing anything.
// app/api/approve/route.ts — called when your coordinator approves a submission
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const submission = await req.json(); // from your own review queue
const res = await fetch('https://api.foodweb.network/v1/suggestions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.FOODWEB_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'create_program',
payload: {
kind: 'event',
name: submission.name,
organizer: { entity_id: submission.organizerEntityId }, // or { name, website, address }
categories: submission.categories,
schedules: [{ frequency: 'dates', dates: [submission.date], start_time: '17:00', end_time: '19:00' }],
source_ref: `sfn:${submission.id}` // makes retries safe
}
})
});
const body = await res.json();
// 201 → status 'applied', published now (inside your area): body.web_url
// 202 → status 'pending', a Food Web curator reviews it: body.reason
return NextResponse.json(body, { status: res.status });
}Errors & caching
401 unauthorizedThe key is unknown or revoked. Check FOODWEB_API_KEY.403 insufficient_scopeThe key lacks map:read or suggestions:write. Ask Food Web to re-issue it.400 validation_errorThe payload or a parameter is wrong; the message names the field. Test pushes with dry_run: true.202 organizer_unresolvedA pushed program names an organizer that is not on the map yet. It is stored and waits for a curator.429 rate_limit_exceeded120 requests a minute per key. Wait Retry-After seconds; cache reads for five minutes.Cache reads in your own server for about five minutes (Next.js revalidate: 300); a new program shows within that window.
Attribution
Show “Program data from Food Web” with a link to https://foodweb.network/events on pages that list programs.