# The manifest, plugin.json

plugin.json, decided in order, then every field from Kanboombo's own schema, every limit, and what changed in contract 1. Unknown keys are refused.

`plugin.json` is the first thing a reviewer reads and the only thing the
Server trusts: your fetcher cannot reach a host you did not declare, nor write a
series you did not name. Add
`"$schema": "https://kanboombo.com/developers/plugins/plugin.schema.json"` and
your editor completes and checks it as you type. Unknown keys are refused at
every level, except `$schema`: a misspelt `fieldnames` fails with
`PLG-MANIFEST-UNKNOWN-KEY` instead of being quietly dropped.

## Decide it in this order

**Who it is.** `id` is 2 to 48 lowercase letters, digits and hyphens, starting
with a letter: your folder's name, part of lasting addresses
(`kanboombo://plugins/<id>`), never reused, so make it plain and lasting. `name`
is per language; `version` is `major.minor.patch`.

**When it runs.** `timezone`, a named IANA zone, never an offset
([Dates](/developers/plugins/concepts/dates-and-time-zones/)); `schedule`, a
five-field cron in it, at most 24 runs a day; `backfill`, how far back the first
run fills: `all`, or a span such as `90d`.

**Who may read it.** `audience` is `public`, `private` or `local`:
[Audiences](/developers/plugins/plan/data-rights/#audiences).

**What it downloads.** At most 16 `sources`, each a lowercase `name` used once
and an https `url` with a lowercase host. Set `maxBytes`, at most 20 MB, close
to what you see: a far larger file is more likely an error page than news.

**What it writes.** 1 to 64 `series`, each one kind of record:

| Decide | Choose |
|---|---|
| `kind` | `value`, numbers about a date; `event`, something that happens on a date, maybe scheduled ahead (a rate decision, with the new rate as its `value`); `item`, a dated document to open |
| `dates` | `observation`, the day described, where a changed number is a revision; `publication`, the day published, with `period` and `revises` (most monthly statistics) |
| Fields | `fields`, 1 to 16 in the order shown; `fieldNames` per language; `unit`, at most 16 characters (`%` shows changes in basis points); `change`, the field whose change readers see, never coloured good or bad |
| `snapshot` | A window, `{ "back": "30d", "ahead": "0d" }`, inside which every run prints the whole truth: for a source that corrects or removes what it published, not for notices that stand; kept inside what the source shows |

`cadence` tells readers what to expect and schedules nothing.
`links.hosts` lists hosts items may link to besides your sources'; `limits` may
lower `seconds` and `megabytes`, never raise them. `licence` says whose data it
is: [Data rights](/developers/plugins/plan/data-rights/).

## Mistakes the check catches {#mistakes-the-check-catches}

`check` reports the first problem of each part, with its code, so most are
fixed in one pass. An offset is not a time zone:

```json error=PLG-MANIFEST-TIMEZONE
{
  "contract": 1, "id": "my-plugin", "name": { "en": "Mine" }, "version": "1.0.0",
  "timezone": "-04:00", "audience": "private", "schedule": "0 7 * * *", "backfill": "all",
  "sources": [{ "name": "data", "url": "https://example.com/data.csv" }],
  "series": [{ "key": "main", "kind": "value", "cadence": "daily", "dates": "observation", "name": { "en": "Main" }, "fields": ["value"] }],
  "licence": { "holder": "Someone", "terms": null, "retain": "unknown" }
}
```

An `http` source fails with `PLG-MANIFEST-SOURCE-URL`; an id with capitals or
an underscore with `PLG-MANIFEST-ID`.

## Every field

### PluginManifest {#pluginmanifest}

plugin.json: what your plug-in downloads, what it writes, when it runs and who may read it. Unknown keys are refused.

| Field | Type | Required | Description |
|---|---|---|---|
| `$schema` | string; at most 500 characters |  | Your editor's schema for plugin.json, such as https://kanboombo.com/developers/plugins/plugin.schema.json. Ignored by Kanboombo. |
| `contract` | `1` | yes | The contract this plug-in follows: 1. |
| `id` | [PluginId](/developers/plugins/reference/manifest/#pluginid) | yes | The plug-in's id, the same as its folder's name. |
| `name` | [PluginNames](/developers/plugins/reference/manifest/#pluginnames) | yes | Its name per language, 80 characters or fewer each. |
| `version` | string | yes | major.minor.patch. Raise it with every change you publish. |
| `timezone` | string; 1 to 100 characters | yes | A named IANA zone. It decides today, how schedule is read, and how source URLs are filled. |
| `audience` | [PluginAudience](/developers/plugins/reference/manifest/#pluginaudience) | yes | Who may read it: public, private or local. |
| `schedule` | string; at most 100 characters | yes | Five-field cron, read in the manifest's time zone. |
| `backfill` | `"all"` or [PluginWindow](/developers/plugins/reference/manifest/#pluginwindow) | yes | How far back the first run fills: all, or a span such as 5y. |
| `sources` | array of [PluginSource](/developers/plugins/reference/manifest/#pluginsource) | yes | The files Kanboombo downloads for each run. At most 16. |
| `series` | array of [PluginSeries](/developers/plugins/reference/manifest/#pluginseries) | yes | What it writes. 1 to 64. |
| `links` | [PluginLinks](/developers/plugins/reference/manifest/#pluginlinks) |  | Where an item's link may point besides your sources' own hosts. |
| `limits` | [PluginLimits](/developers/plugins/reference/manifest/#pluginlimits) |  | Lower time and memory limits, if you want them. |
| `licence` | [PluginLicence](/developers/plugins/reference/manifest/#pluginlicence) | yes | Who holds the data, how it is credited, and whether it may be kept. |

### PluginSource {#pluginsource}

One file Kanboombo downloads before each run and hands to your fetcher. https only; the host refuses private addresses and redirects to hosts no source names. {today}, {year} and {month} are filled in the manifest's time zone.

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | [PluginKey](/developers/plugins/reference/manifest/#pluginkey) | yes | The source's name: the key your fetcher finds it under in the run context's sources. |
| `url` | string; at most 500 characters | yes | Where to download it: https, with a lowercase host. |
| `maxBytes` | integer; 1 to 20,971,520 |  | The largest download you expect, at most 20 MB (the default). A larger file fails the run. |

### PluginSeries {#pluginseries}

One kind of record your plug-in writes, and how readers see it.

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | [PluginKey](/developers/plugins/reference/manifest/#pluginkey) | yes | The series key every record of it names. |
| `kind` | [PluginRecordKind](/developers/plugins/reference/manifest/#pluginrecordkind) | yes | value, event or item. |
| `cadence` | [PluginCadence](/developers/plugins/reference/manifest/#plugincadence) | yes | How often it has a new record. |
| `dates` | [PluginDates](/developers/plugins/reference/manifest/#plugindates) | yes | Whether a record is dated by what it describes (observation) or by when it was published (publication). |
| `name` | [PluginNames](/developers/plugins/reference/manifest/#pluginnames) | yes | Its name per language, shown as the row's label. |
| `unit` | string; at most 16 characters |  | What its numbers are in, such as DOP, m or %. Up to 16 characters. Screen readers say it after the figures; with % a change is shown in basis points and an event's value as a percentage. |
| `fields` | array of [PluginKey](/developers/plugins/reference/manifest/#pluginkey) |  | A value series' numbers, by key, in the order they are shown. 1 to 16. |
| `fieldNames` | object of [PluginNames](/developers/plugins/reference/manifest/#pluginnames) |  | A label for each field, per language, for example compra: { es: Compra, en: Buy }. |
| `change` | [PluginKey](/developers/plugins/reference/manifest/#pluginkey) |  | The field whose change from the previous record is shown, never coloured as good or bad. |
| `snapshot` | [PluginSnapshot](/developers/plugins/reference/manifest/#pluginsnapshot) |  | A window each run fully restates: a stored record missing from it for three runs is withdrawn. |

### PluginSnapshot {#pluginsnapshot}

Each run's batch is the whole truth for this window. A stored record inside it that is missing from three runs in a row is withdrawn.

| Field | Type | Required | Description |
|---|---|---|---|
| `back` | [PluginWindow](/developers/plugins/reference/manifest/#pluginwindow) | yes | How far before today the window reaches. |
| `ahead` | [PluginWindow](/developers/plugins/reference/manifest/#pluginwindow) | yes | How far after today it reaches, for scheduled events. |

### PluginLinks {#pluginlinks}

The hosts an item record may link to, besides every source's host.

| Field | Type | Required | Description |
|---|---|---|---|
| `hosts` | array of string | yes | Lowercase host names, such as www.bancentral.gov.do. At most 16. |

### PluginLimits {#pluginlimits}

Lower limits than the defaults, if your fetcher needs less. They can never be raised.

| Field | Type | Required | Description |
|---|---|---|---|
| `seconds` | integer; 1 to 120 |  | How long a run may take, 1 to 120. Default 120. |
| `megabytes` | integer; 16 to 256 |  | How much memory a run may use, 16 to 256. Default 256. |

### PluginLicence {#pluginlicence}

Who holds the data and what you may do with it. Readers see the attribution beside every record.

| Field | Type | Required | Description |
|---|---|---|---|
| `holder` | string; 1 to 200 characters | yes | Who publishes the data, such as Banco Central de la República Dominicana. |
| `attribution` | string; 1 to 40 characters |  | The short name shown beside its records, such as Banco Central. |
| `terms` | string or null; at most 500 characters | yes | A link to the terms the data is published under, or null when there are none to link. |
| `retain` | `"yes"`, `"no"`, `"unknown"` | yes | Whether Kanboombo may keep the records: yes, no (they can be purged) or unknown. |

### PluginId {#pluginid}

A plug-in's id: lowercase letters, digits and hyphens, starting with a letter, 2 to 48 characters. It is also its folder's name, and it is never reused.

- Type: string
- Pattern: `^[a-z][a-z0-9-]{1,47}$`
- Example: `dop-economy`

### PluginKey {#pluginkey}

A key for a source, a series or a field: lowercase letters, digits, hyphens and underscores, starting with a letter.

- Type: string
- Pattern: `^[a-z][a-z0-9_-]{0,47}$`
- Example: `usd-dop`, `compra`

### PluginNames {#pluginnames}

A name in each language the plug-in speaks, keyed by language code. A reader sees their own language, then en, then the first listed.

- Type: object of string

### PluginAudience {#pluginaudience}

public: anyone signed in, and a Mac's proxy. private: the readers set at host.kanboombo.com (people and organizations). local: runs on your Mac only; refused by the Server until its slice ships.

- Type: `"public"`, `"private"`, `"local"`

### PluginCadence {#plugincadence}

How often the series has a new record. It tells readers what to expect; it does not schedule anything.

- Type: `"business-day"`, `"daily"`, `"weekly"`, `"monthly"`, `"quarterly"`, `"irregular"`

### PluginRecordKind {#pluginrecordkind}

value: numbers on a date. event: something scheduled, happened or cancelled. item: a dated link with a title.

- Type: `"value"`, `"event"`, `"item"`

### PluginDates {#plugindates}

observation: a record's date is the day it describes. publication: the day it was published, with period naming what it describes.

- Type: `"observation"`, `"publication"`

### PluginWindow {#pluginwindow}

A span relative to the run's today: a number and a unit, d for days or y for years.

- Type: string
- Pattern: `^[0-9]{1,4}[dy]$`
- Example: `60d`, `400d`, `5y`

## Limits {#limits}

The checker's numbers, the same in `check` and on the Server:

| Limit | Value | What it holds |
|---|---|---|
| `sources` | 16 | Sources one manifest may declare |
| `sourceBytes` | 20,971,520 (20 MB) | The largest download one source may be (`maxBytes`), in bytes |
| `sourceUrl` | 500 | Characters in a source's `url` |
| `series` | 64 | Series one manifest may declare |
| `fields` | 16 | Fields in a value series, and in one value record |
| `linkHosts` | 16 | Hosts in `links.hosts` |
| `name` | 80 | Characters in a name, in each language |
| `unit` | 16 | Characters in a series' `unit` |
| `attribution` | 40 | Characters in `licence.attribution` |
| `holder` | 200 | Characters in `licence.holder` |
| `terms` | 500 | Characters in `licence.terms` |
| `lines` | 250,000 | Records one run may print |
| `period` | 32 | Characters in a record's `period` |
| `id` | 64 | Characters in an event's or an item's `id` |
| `title` | 200 | Characters in a `title` |
| `url` | 1,000 | Characters in an item's `url` |
| `seconds` | 1 to 120; 120 if you set nothing | How long one run may take, in seconds (`limits.seconds`) |
| `megabytes` | 16 to 256; 256 if you set nothing | How much memory one run may use, in megabytes (`limits.megabytes`) |

### What the Server holds a run to {#what-the-server-holds-a-run-to}

| What | Number |
|---|---|
| Time and memory | 120 seconds and 256 MB. A lower `limits.seconds` is kept; a lower `limits.megabytes` is not yet enforced on the Server, though `check` enforces both |
| Runs a day | At most 24 (`PLG-SCHEDULE-TOO-FREQUENT` at upload) |
| A withdrawal | A stored record missing from its snapshot window for 3 runs in a row |
| Withdrawals that wait for a person | More than 10 at once, or more than 1% of a window of 100 or more |
| Days a reader asks for at once | 62 |
| Records one run may print | 250,000 |
| The time zone | A named IANA zone, up to 100 characters |

## Contract 1 and its changes {#changelog}

Contract 1 only grows: a field, a stated limit or a check with its own code may
be added, and nothing a plug-in relies on is taken away or changes meaning. A
change that would break a working plug-in is contract 2, run beside contract 1.
A check code is never renamed or reused; a retired one stays in
[Submission errors](/developers/plugins/publish/submission-errors/).

**October 2026.** Contract 1, with DOP Economy on readers' days. Added the same
week, changing nothing DOP Economy writes nor its digest: `$schema` ([plugin.schema.json](/developers/plugins/plugin.schema.json));
`links.hosts`; `backfillFrom` in the run context; `fixtures/context.json` and
`fixtures/runs/<case>/`; the digest leaving out `listing.json`, `listing/`,
`review.json`, `.git/`, `.DS_Store`, `AGENTS.md` and `CLAUDE.md`; and a code
from one catalog, with its fix and page, on every problem. The tool's versions
are in [the CLI](/developers/plugins/reference/cli/#versions).
