Offers on your website
The published offers of your holiday programme can be read without a token. This guide shows how to put them on a municipal website, a partner's site or an app, and how to keep them up to date.
Embeds will put the offers on your website with a script and one HTML element, no server code needed. They are a prototype for now; until they are released, use the API as described here.
What you get
Without a token, GET /offers returns the offers the public fepli website shows:
- offers that are published,
- in an edition that is online,
- and public, not reachable through a private link only.
Each offer comes with its public fields: name, teaser, description, dates, fee, age range, meeting point, places left, images, categories, organisers and the link to its page on the fepli website. Internal fields such as the contact person or the application counts are left out.
Public access must be switched on under Einstellungen → Integrationen → API → Öffentlicher Zugriff. It is on by default.
Fetch on your server, not in the browser
Anonymous requests are limited to 5 per hour per IP address (see Rate limits). Calling the API from your visitors' browsers would therefore work for the first few visitors of an office network and then fail. Instead:
- Fetch the offers on your server.
- Cache the result, in a file, a database or your CMS's cache.
- Serve your pages from the cache and refresh it every few hours.
A complete refresh is also the simplest way to stay correct: offers that were unpublished or deleted just don't come back.
Fetch and cache the offers of an edition
import { readFile, writeFile } from 'node:fs/promises'
const API = 'https://ferienpass-musterstadt.de/api'
const CACHE = './offers.json'
const MAX_AGE = 3 * 60 * 60 * 1000 // three hours
export async function getOffers() {
try {
const cached = JSON.parse(await readFile(CACHE, 'utf8'))
if (Date.now() - cached.fetchedAt < MAX_AGE) return cached.offers
} catch {}
const offers = []
for (let page = 1; ; page++) {
const query = new URLSearchParams({
edition: 'herbstferien-2026',
itemsPerPage: 200,
page,
})
const response = await fetch(`${API}/offers?${query}`)
if (!response.ok) throw new Error(`fepli API: ${response.status}`)
const batch = await response.json()
offers.push(...batch)
if (batch.length < 200) break
}
await writeFile(CACHE, JSON.stringify({ fetchedAt: Date.now(), offers }))
return offers
}
With 200 offers per page, an edition of 350 offers takes two requests per refresh, well within the limit.
Choose what to show
Narrow the list with filters. They can be combined:
| You want | Use |
|---|---|
| one edition | edition=herbstferien-2026 (alias or UUID) |
| one category | category=sport-bewegung |
| one organiser | host=stadtjugendring-musterstadt |
| offers on certain days | from=2026-10-12&until=2026-10-18 (for offers with several dates, see the filter) |
| a search box | q=Fahrrad |
| a different order | sort=name, sort=begin&order=desc |
Find the aliases of editions, categories and organisers in the offers themselves: every offer carries edition, categories and hosts as references with uuid, name and alias.
Build an offer card
The fields you'll most likely use:
| Field | How to show it |
|---|---|
name, teaser | title and subtitle |
description | Markdown. Convert it to HTML with a Markdown library. |
dates | a list of begin/end timestamps. An offer can have several dates. |
fee | cents. 750 is €7.50; missing or 0 means free. |
minAge, maxAge | "for ages 8 to 12". Either can be missing. |
vacancies, fullyBooked | places left. vacancies is missing when places are unlimited. |
requiresApplication, applicationDeadline | whether and until when families have to apply |
meetingPoint, meetingAddress, meetingLat, meetingLng | where to go, for a map |
media | absolute URLs of the offer's images, at most 3000 × 3000 pixels |
status | contains cancelled when a published offer was called off. Show it as cancelled rather than dropping it. |
url | the offer's page on the fepli website |
Families apply on the fepli website, not on yours. Link each card to its url, and they land on the offer's page with the application form.
An offer, as anonymous callers see it
{
"uuid": "0192a4f1-6e3b-7c85-b1d2-8f4e6a9c3b03",
"name": "Fahrradtour durch den Stadtpark",
"alias": "fahrradtour-durch-den-stadtpark",
"edition": {
"uuid": "01913e5c-2b4a-7d10-9f3e-6a1c2e7b4d01",
"name": "Herbstferien 2026",
"alias": "herbstferien-2026"
},
"hosts": [
{
"uuid": "01905a7e-8c21-7b3f-a0d4-3e9f1c6b2a02",
"name": "Stadtjugendring Musterstadt e.V.",
"alias": "stadtjugendring-musterstadt"
}
],
"status": ["published"],
"teaser": "Mit dem Rad durch den Park – mit Picknick am See.",
"description": "Eine **geführte Fahrradtour** für Kinder ab 8 Jahren.\n\nBitte mitbringen:\n\n- Fahrrad\n- Helm",
"dates": [
{
"uuid": "0192a4f2-0a11-7e6c-9d38-2b7f5c1e8a04",
"begin": "2026-10-14T09:00:00+02:00",
"end": "2026-10-14T12:00:00+02:00"
}
],
"fee": 750,
"minAge": 8,
"maxAge": 12,
"maxParticipants": 15,
"vacancies": 4,
"fullyBooked": false,
"requiresApplication": true,
"onlineApplication": true,
"applicationDeadline": "2026-09-30T23:59:59+02:00",
"meetingPoint": "Haupteingang Stadtpark",
"meetingAddress": "Parkstraße 1, 12345 Musterstadt",
"meetingLat": 52.520008,
"meetingLng": 13.404954,
"bring": "Fahrrad und Helm",
"wheelchairAccessible": false,
"categories": [
{
"uuid": "018f2c3d-4e5f-7a6b-8c7d-9e0f1a2b3c12",
"name": "Sport & Bewegung",
"alias": "sport-bewegung"
}
],
"url": "https://ferienpass-musterstadt.de/angebote/herbstferien-2026/1C8wV6pQk2Tz9RfXh4MbNd-fahrradtour-durch-den-stadtpark",
"media": [
"https://ferienpass-musterstadt.de/assets/images/f/fahrradtour-3000x2000.jpg"
],
"createdAt": "2026-08-03T10:12:44+02:00",
"modifiedAt": "2026-09-15T16:40:02+02:00"
}
Fields without a value are left out of the response. Treat a missing field as empty. See Conventions.
Cancelled offers, drafts and private offers
A published offer that is called off stays in the list, with status reading ["published", "cancelled"], so families who saw it can find out it was cancelled.
Offers that are still being prepared and offers published only through a private link are never returned without a token. If your integration needs them, for example an organiser's own site showing their drafts, use a token with read access.