> ## Documentation Index
> Fetch the complete documentation index at: https://docs.campfire.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# App manifest

> Define your whole OAuth app, from details and permissions to buttons, panels, navigation and events, as one JSON document

A **manifest** is your app's complete definition as one JSON document: its details and permissions, allowed domains, [buttons, panels, navigation, and event subscriptions](/guides/app-extensions). Keep it in your repository next to your code, review changes like any other change, and apply it to Campfire in one step.

<CardGroup cols={2}>
  <Card title="FakeTax manifest.json" icon="github" href="https://github.com/Campfire-eng/campfire_faketax_app/blob/main/manifest.json">
    The sample app's manifest: five buttons, two panels, a sidebar section, and event subscriptions.
  </Card>

  <Card title="Extending Campfire" icon="puzzle-piece" href="/guides/app-extensions">
    What each extension does and how your server handles it.
  </Card>
</CardGroup>

## Applying a manifest

### Creating a new app

On [Settings > Developer > Apps](https://app.meetcampfire.com/v2/settings/apps), click **From Manifest** (or **Create from Manifest** if you have no apps yet), paste your manifest, and click **Create App**.

<Frame>
  <img src="https://mintcdn.com/campfire/EZaYuU9NXAcXYXMc/images/oauth-apps/from-manifest-dialog.png?fit=max&auto=format&n=EZaYuU9NXAcXYXMc&q=85&s=eba37fdd4f8771efb3d0d703804eed9f" alt="The Create app from manifest dialog with a JSON text area" width="1800" height="1125" data-path="images/oauth-apps/from-manifest-dialog.png" />
</Frame>

The `app` section becomes **version 1** in production straight away, and the extensions are created with it. Campfire then shows the client secret once, as when you [create an app by hand](/guides/oauth-apps#part-1-register-your-app). `app.allowed_scopes` is required when creating from a manifest. Use `[]` for an app with no permissions.

### Updating an existing app

Open the app's **Manifest** tab. It shows the app's current manifest, which you can copy into your repository. Paste the new version and click **Save Manifest**.

<Frame>
  <img src="https://mintcdn.com/campfire/EZaYuU9NXAcXYXMc/images/oauth-apps/app-manifest.png?fit=max&auto=format&n=EZaYuU9NXAcXYXMc&q=85&s=c16198e9eff3e9905cdb3a5747b8abcb" alt="The Manifest tab showing FakeTax's manifest as JSON with a Save Manifest button" width="1800" height="1125" data-path="images/oauth-apps/app-manifest.png" />
</Frame>

Saving is **all or nothing**. If any part is invalid, nothing changes and you get an error naming the part (see [Errors](#errors)). When it succeeds:

* **The `app` section is versioned.** Fields that differ from the version you're editing go to the app's development version, which goes live only when you [promote it](/guides/oauth-apps#versions-and-approval). If nothing in `app` changed, no draft is opened.
* **Everything else takes effect immediately**, in every workspace where your app is connected: allowed domains, buttons, panels, navigation, and event subscriptions.
* **The manifest is the full list.** Buttons, panels, and navigation entries are matched by `key`, and subscriptions by `endpoint_url`. Anything that isn't in the manifest is **deleted**.
* **Workspace settings survive.** Because items are matched by key, a workspace admin's switches and style choices stay in place when you re-apply. Changing a key creates a new item, which loses them.
* **Allowed domains are applied first**, so one manifest can add a domain and the button that uses it.

<Warning>
  Leaving out `slots`, `panels`, `navigation`, or `event_subscriptions` is the same as sending an empty list: everything in that section is deleted. Leaving out `app` or `allowed_domains` leaves them unchanged.
</Warning>

### Over the API

The same operations are available to a workspace admin's session at `https://api.meetcampfire.com`:

| Method and path | Does |
| - | - |
| `GET /ca/api/v1/oauth-apps/<app_id>/manifest` | Export the manifest. The `app` section shows the development version if there is one. |
| `PUT /ca/api/v1/oauth-apps/<app_id>/manifest` | Apply a manifest. Returns the manifest as saved. |
| `POST /ca/api/v1/oauth-apps/from-manifest` | Create an app. Returns the app, its `client_secret` (shown once), and its `manifest`. |
| `GET /ca/api/v1/oauth-apps/extension-catalog` | Object types, the fields each can test in `visible_when`, and the event topics. |

<Note>App management endpoints are never available to OAuth app tokens.</Note>

## Reference

A manifest is a JSON object with up to six sections:

```json theme={null}
{
  "app": { },
  "allowed_domains": [],
  "slots": [],
  "panels": [],
  "navigation": [],
  "event_subscriptions": []
}
```

### `app`

The app's versioned details. Only these keys are allowed:

| Key | Type | Notes |
| - | - | - |
| `name` | string | Required. |
| `tagline` | string | Up to 120 characters. |
| `description` | string | Up to 2,000 characters. |
| `icon_url` | string or `null` | Must be an icon already uploaded to Campfire. Upload one on the **Details** tab, then export to get its URL. |
| `redirect_uris` | string\[] | Up to 20. See [redirect URL rules](/guides/oauth-apps#part-1-register-your-app). |
| `connect_url` | string | Where **Connect** on the connection page sends people. |
| `manage_url` | string | Where **Manage in \<App>** sends people. See [Manage URL](/guides/app-extensions#manage-url). |
| `allowed_scopes` | string\[] | The app's permission ceiling, using [scope names](/guides/oauth-apps#scopes-reference) such as `invoices:write`. |

### `allowed_domains`

Up to 20 host names. `example.com` matches only that host. `*.example.com` matches its subdomains, but not `example.com`. Every URL in the sections below must be on one of these hosts.

### `slots` (buttons)

| Key | Type | Notes |
| - | - | - |
| `key` | string | Unique within the app. Up to 64 characters, slug format. |
| `label` | string | Up to 60 characters. |
| `icon` | string | A [Lucide](https://lucide.dev/icons) icon name, for example `calculator`. |
| `object_type` | string | `invoice`, `bill`, `journal_entry`, or `vendor`. |
| `surface` | string | `detail_header`, `list_header`, or `row_menu`. |
| `requested_variant` | string | `menu` (default), `secondary`, or `primary`. |
| `action_type` | string | `link`, `webhook`, or `iframe`. |
| `action_config` | object | Depends on `action_type`. See below. |
| `visible_when` | object\[] | Up to 5 rules: `{"field", "op", "value"}`, with `op` one of `eq`, `neq`, `in`, `not_in`. |
| `confirm_text` | string | Up to 200 characters. Empty for no confirmation. |
| `order` | integer | Sort order within the same placement. |
| `is_active` | boolean | `false` hides the button everywhere. Must be a real boolean. |

`action_config` by action type:

| `action_type` | `action_config` |
| - | - |
| `link` | `{"url_template"}` |
| `webhook` | `{"url_template", "timeout_ms"}`. `timeout_ms` is 1000 to 10000 (default 8000). |
| `iframe` | `{"url_template", "presentation", "width"}`. `presentation` is `drawer` (default) or `modal`. `width` is `sm`, `md` (default), or `lg`. |

URL templates can be up to 1,024 characters and use `{{record.id}}`, `{{record.number}}`, `{{customer.id}}`, and `{{install.<key>}}`. `{{record.*}}` isn't allowed on `list_header`.

### `panels`

| Key | Type | Notes |
| - | - | - |
| `key` | string | Unique within the app. |
| `label` | string | The tab title. Up to 60 characters. |
| `icon` | string | Lucide icon name. |
| `object_type` | string | `invoice`, `bill`, `journal_entry`, or `vendor`. |
| `placements` | string\[] | One or more of `list`, `detail`, `edit`. |
| `url_template` | string | `{{record.*}}` isn't allowed when `list` is a placement. |
| `visible_when` | object\[] | As for buttons. |
| `order` | integer | Sort order. |
| `is_active` | boolean | |

### `navigation`

A list of top-level entries. Each entry can hold one level of `items`:

| Key | Type | Notes |
| - | - | - |
| `key` | string | Unique across all entries and items. |
| `label` | string | Up to 40 characters. |
| `icon` | string | Lucide icon name. |
| `url_template` | string | Empty for a section that only groups items. Required on items. Only `{{customer.*}}` and `{{install.*}}`. |
| `order` | integer | Optional. Defaults to the entry's position in the list. |
| `is_active` | boolean | |
| `items` | object\[] | Top-level entries only. Items have the same keys, without `items`. |

At most 30 entries in total, counting items.

### `event_subscriptions`

| Key | Type | Notes |
| - | - | - |
| `endpoint_url` | string | Where events are posted. Only `{{customer.*}}` and `{{install.*}}`. Identifies the subscription when you re-apply. |
| `topics` | string\[] | Non-empty. Each must be covered by `app.allowed_scopes`. See [topics](/guides/app-extensions#event-subscriptions). |
| `is_active` | boolean | |

## Errors

Saving an invalid manifest returns `400` with a `detail` that says where the problem is:

| Example `detail` | Fix |
| - | - |
| `app: not part of the manifest: client_id` | Remove keys that aren't listed under [`app`](#app). |
| `slots[2]: visible_when: payment_status cannot be tested on vendor; use country, is_1099, vendor_type` | Use a field from the [visibility table](/guides/app-extensions#visibility-rules). |
| `Slot keys must be unique.` | Give every button its own `key`. Panels and navigation work the same way. |
| `Too many slots, panels, navigation entries or subscriptions.` | Stay within the [limits](/guides/app-extensions#limits). |
| `slots, panels, navigation and event_subscriptions must be lists.` | Use `[]`, not `{}` or `null`. |
| `allowed_domains: ...` | Fix the host name. |

A URL that isn't on an allowed domain, a topic your scopes don't cover, or a `{{record.*}}` placeholder where it isn't allowed is reported against the item it's in.

## Example: FakeTax

This is FakeTax's complete [`manifest.json`](https://github.com/Campfire-eng/campfire_faketax_app/blob/main/manifest.json). It runs on `localhost`, so every URL uses `http://localhost:3999`. In production, use your own `https` host.

<Accordion title="manifest.json">
  ```json manifest.json theme={null}
  {
    "app": {
      "name": "FakeTax",
      "tagline": "Made-up sales tax on invoices",
      "description": "Sales tax for invoices and credit memos by jurisdiction, with tax codes on lines. The tax is fake; the plumbing is real.",
      "icon_url": null,
      "redirect_uris": [
        "http://localhost:3999/callback"
      ],
      "connect_url": "http://localhost:3999/connect",
      "allowed_scopes": [
        "invoices:read",
        "invoices:write",
        "credit_memos:write",
        "custom_fields:write",
        "vendors:read"
      ],
      "manage_url": "http://localhost:3999/manage"
    },
    "allowed_domains": [
      "localhost"
    ],
    "slots": [
      {
        "key": "view",
        "label": "View in FakeTax",
        "icon": "external-link",
        "object_type": "invoice",
        "surface": "detail_header",
        "requested_variant": "menu",
        "action_type": "link",
        "action_config": {
          "url_template": "http://localhost:3999/report/{{record.id}}"
        },
        "visible_when": [],
        "confirm_text": "",
        "order": 2,
        "is_active": true
      },
      {
        "key": "calculate",
        "label": "Calculate tax",
        "icon": "calculator",
        "object_type": "invoice",
        "surface": "detail_header",
        "requested_variant": "secondary",
        "action_type": "webhook",
        "action_config": {
          "url_template": "http://localhost:3999/slots/calculate",
          "timeout_ms": 8000
        },
        "visible_when": [
          {
            "field": "payment_status",
            "op": "in",
            "value": [
              "draft",
              "open"
            ]
          }
        ],
        "confirm_text": "Overwrite tax on every line?",
        "order": 0,
        "is_active": true
      },
      {
        "key": "calculate-bulk",
        "label": "Calculate FakeTax",
        "icon": "calculator",
        "object_type": "invoice",
        "surface": "list_header",
        "requested_variant": "menu",
        "action_type": "webhook",
        "action_config": {
          "url_template": "http://localhost:3999/slots/calculate",
          "timeout_ms": 8000
        },
        "visible_when": [],
        "confirm_text": "",
        "order": 0,
        "is_active": true
      },
      {
        "key": "open-row",
        "label": "FakeTax report",
        "icon": "external-link",
        "object_type": "invoice",
        "surface": "row_menu",
        "requested_variant": "menu",
        "action_type": "link",
        "action_config": {
          "url_template": "http://localhost:3999/report/{{record.id}}"
        },
        "visible_when": [],
        "confirm_text": "",
        "order": 0,
        "is_active": true
      },
      {
        "key": "breakdown",
        "label": "Tax breakdown",
        "icon": "table",
        "object_type": "invoice",
        "surface": "detail_header",
        "requested_variant": "menu",
        "action_type": "iframe",
        "action_config": {
          "url_template": "http://localhost:3999/frame/breakdown/{{record.id}}",
          "presentation": "drawer",
          "width": "lg"
        },
        "visible_when": [],
        "confirm_text": "",
        "order": 1,
        "is_active": true
      }
    ],
    "panels": [
      {
        "key": "breakdown-tab",
        "label": "FakeTax",
        "icon": "table",
        "object_type": "invoice",
        "placements": [
          "detail",
          "edit"
        ],
        "url_template": "http://localhost:3999/frame/breakdown/{{record.id}}",
        "visible_when": [],
        "order": 0,
        "is_active": true
      },
      {
        "key": "settings",
        "label": "FakeTax",
        "icon": "settings",
        "object_type": "invoice",
        "placements": [
          "list"
        ],
        "url_template": "http://localhost:3999/frame/settings",
        "visible_when": [],
        "order": 0,
        "is_active": true
      }
    ],
    "navigation": [
      {
        "key": "faketax",
        "label": "FakeTax",
        "icon": "calculator",
        "url_template": "",
        "is_active": true,
        "items": [
          {
            "key": "jurisdictions",
            "label": "Jurisdictions",
            "icon": "map-pin",
            "url_template": "http://localhost:3999/frame/manage",
            "is_active": true
          },
          {
            "key": "tax-codes",
            "label": "Tax codes",
            "icon": "tag",
            "url_template": "http://localhost:3999/frame/codes",
            "is_active": true
          },
          {
            "key": "settings",
            "label": "Settings",
            "icon": "settings",
            "url_template": "http://localhost:3999/frame/settings",
            "is_active": true
          }
        ]
      }
    ],
    "event_subscriptions": [
      {
        "topics": [
          "Invoice.created",
          "Invoice.updated",
          "Invoice.deleted",
          "CreditMemo.created",
          "CreditMemo.updated"
        ],
        "endpoint_url": "http://localhost:3999/events",
        "is_active": true
      }
    ]
  }
  ```
</Accordion>

It defines:

* **Five buttons** on invoices: **Calculate tax** (a secondary webhook button on draft and open invoices, with a confirmation), **Calculate FakeTax** (the same webhook for selected rows on the list), **Tax breakdown** (a large drawer), **View in FakeTax** (a link), and **FakeTax report** (a link in the row menu).
* **Two panels**: the breakdown on the invoice page and edit modal, and settings on the invoice list.
* **A sidebar section** with three pages.
* **One subscription** for invoice and credit memo changes.
