Introduction
confish is the missing UI for the things you run - bots, scripts, cron jobs, workers. One REST API covers four surfaces: typed configuration your code fetches on demand, one-off actions it picks up on the next poll, logs you push up, and live feeds of whatever you produce.
Instead of editing .env files over SSH, tailing logs by hand, or building a small admin panel per project, you get one place for all of it: typed config, searchable logs, live feeds of whatever your code produces, and one-off actions your workers pick up - each with its own REST endpoints, plus webhooks when things change.
Base URL
https://confi.shQuick Start
Create an application
Sign up and create your first application from the dashboard. An application is an isolated container for your environments and the schema they share - config fields and feed definitions.
Define your fields
Serving configuration? Define its fields - each has a key, a type (string, number, boolean, date, or array), and optional constraints, shared across every environment. Feeds carry their own shape, and logs and actions need no schema - skip ahead if that's where you're starting.
Create environments
Create environments like "Production", "Staging", or "Development". Each gets its own API key and holds its own data across every surface - config values, feed items, logs, and actions.
Make your first call
Everything runs through your environment's API key. Fetch typed config, push a log, upsert a feed item, or pick up an action - each is a single REST call. Here's config to get you started:
curl "https://confi.sh/c/a1b2c3d4e5f6" \ -H "Authorization: Bearer confish_sk_your_api_key"See Logging, Feeds, and Actions below for the other surfaces.
Core Concepts
Applications
An application is the top-level container. It holds your field definitions (the schema), your feed definitions, and one or more environments. You might create one application per project, per microservice, or per client - whatever makes sense for your use case.
Field Definitions
Field definitions describe the shape of your configuration. Each field has a unique key, a display name, a type, and optional type-specific options (like min/max for numbers or date formats for dates). The schema is shared across all environments in an application - changing a field definition affects every environment.
Environments
Environments are where the actual data lives: configuration values, queued actions, logs, and feed items. Each environment has its own API key and optionally a webhook URL. You might have "Production", "Staging", and "Development" environments - different values, same schema.
Configuration
The typed values an environment serves to your code at runtime. Set them here or through the API and read them back with a single fetch - change a value without shipping a new build.
Feeds
Typed collections you push living state into - job runs, crawl results, incidents, sensor readings. Define the shape once, then upsert items by ID from your code; they appear in real time and can expire on a TTL.
Logs
Structured log lines your scripts send to an environment. Search and tail them live, or share them on a public feed - a lightweight window into what your automations are doing.
Actions
One-off commands you dispatch to an environment for your code to pick up over the API. Each one is tracked through its lifecycle - pending, acknowledged, in-progress, complete - with progress updates along the way.
SDKs
Official SDKs are available for the most common backend languages. Each one wraps the REST API with typed methods, typed errors, automatic retries, an action consumer with adaptive backoff, and a webhook signature verifier. If your language isn't listed, the REST API is documented below and works from anywhere.
TypeScript / JavaScript
@confish/sdkWorks in Node 18+, Bun, Deno, Cloudflare Workers, Vercel Edge. Typed config via generic.
npm install @confish/sdkPython
confishPython 3.10+. Single httpx dependency, fully type-hinted, thread-based action consumer.
pip install confishGo
confish-goGo 1.22+. Standard library only, context-aware, errors.As-friendly typed errors.
go get github.com/confishhq/confish-goPHP
confish/sdkPHP 8.1+. Built on Guzzle, integrates cleanly with Laravel and other frameworks.
composer require confish/sdkRust
confishRust 1.75+. Async on tokio + reqwest, typed config via serde::Deserialize, rustls by default.
cargo add confishEach SDK exposes the same surface: fetch and write configuration, send logs, run an action consumer with graceful shutdown, and verify webhook signatures. The sections below describe the underlying REST API the SDKs call - useful if you're using a language without an SDK or want to understand what's happening under the hood.
Authentication
All API requests are authenticated using Bearer tokens. Each environment has its own API key, prefixed with confish_sk_.
Include the API key in the Authorization header of every request:
Authorization: Bearer confish_sk_your_api_key_hereKeep your API keys secret. Do not expose them in client-side code or public repositories. You can regenerate a key at any time from the environment settings.
Fetch Configuration
/c/{env_id}Returns the environment's configuration values, transformed and typed according to your field definitions. The response is cached for performance and automatically invalidated when values change.
Example Request
curl "https://confi.sh/c/a1b2c3d4e5f6" \ -H "Authorization: Bearer confish_sk_your_api_key"Example Response
{ "site_name": "My Application", "max_upload_mb": 25, "maintenance_mode": false, "launch_date": "2026-03-16", "allowed_origins": ["https://example.com", "https://app.example.com"]}Values are automatically cast to their defined types: strings remain strings, numbers are returned as floats, booleans as true/false, dates are formatted according to the field's format option, and arrays contain typed elements.
Update Configuration
Config write access must be enabled before you can use these endpoints - open the configuration settings via the gear icon on the Configuration tab. It is disabled by default. This toggle only affects configuration writes - feeds and logs are always writable with a valid API key.
/c/{env_id}Replace all configuration values. Any fields not included in the request body will be reset to their defaults. Values are validated against your field definitions.
/c/{env_id}Partially update configuration values. Only the fields you include will be updated - all other fields retain their current values. This is the recommended method for most use cases.
Request Body
valuesobjectrequiredAn object mapping field keys to their new values. Keys must match your field definitions. Values are validated against each field's type and constraints.
Example Request (PATCH)
curl -X PATCH https://confi.sh/c/a1b2c3d4e5f6 \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "values": { "maintenance_mode": true, "max_upload_mb": 50 } }'Example Response
{ "site_name": "My Application", "max_upload_mb": 50, "maintenance_mode": true, "launch_date": "2026-03-16", "allowed_origins": ["https://example.com", "https://app.example.com"]}The response returns the full transformed configuration (same format as GET /c/{env_id}) so you can confirm the result without a second request. Validation errors return a 422 response with field-level error messages.
Write operations trigger webhooks and invalidate the cache, exactly like saving values through the dashboard.
Store Log Entry
/c/{env_id}/logSend log entries from your application to confish. Logs are stored per environment and visible in the dashboard.
Request Body
levelstringrequiredLog level. One of: debug, info, notice, warning, error, critical, alert, emergency.
messagestringrequiredThe log message. Maximum 1000 characters.
contextobjectOptional key-value pairs providing additional context for the log entry.
Example Request
curl -X POST https://confi.sh/c/a1b2c3d4e5f6/log \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "level": "info", "message": "User logged in", "context": { "user_id": 123, "ip": "192.168.1.1" } }'Example Response
{ "id": "01hy8v3kqm..."}Batch Ingestion
The same endpoint also accepts an entries array of up to 100 log entries per request - handy when your application buffers logs locally and flushes them periodically. Each entry takes the same level, message, and optional context fields, plus an optional ISO 8601 timestamp so buffered records keep their true log time instead of the time they were flushed.
curl -X POST https://confi.sh/c/a1b2c3d4e5f6/log \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "entries": [ { "level": "info", "message": "Crawl started", "context": {"job": "sitemap-crawler"}, "timestamp": "2026-07-09T02:14:00Z" }, { "level": "warning", "message": "Slow response from origin", "context": {"url": "https://example.com/sitemap.xml", "ms": 4820}, "timestamp": "2026-07-09T02:14:41Z" }, { "level": "info", "message": "Crawl finished", "context": {"pages": 12480}, "timestamp": "2026-07-09T02:19:03Z" } ] }'The response returns the created log IDs in the same order as the entries:
{ "ids": ["01hy8v3kqm...", "01hy8v3kqn...", "01hy8v3kqp..."]}Rate Limiting
API requests are rate-limited per application based on your plan:
| Plan | Requests per minute |
|---|---|
| Free | 60 |
| Pro | 600 |
When you exceed the limit, the API returns a 429 Too Many Requests response with the following headers:
| Header | Description |
|---|---|
Retry-After | Seconds until you can make requests again |
X-RateLimit-Limit | Your rate limit ceiling |
X-RateLimit-Remaining | Remaining requests in the current window |
Error Responses
The API uses standard HTTP status codes to indicate success or failure:
| Status | Description |
|---|---|
200 | Success |
201 | Created (log entries) |
401 | Missing or invalid API key |
403 | API key doesn't match environment, or application is disabled |
422 | Validation error (invalid request body) |
429 | Rate limit exceeded |
500 | Field misconfiguration or server error |
Error responses include a JSON body with an error field:
{ "error": "Missing API key"}Field Types
Field definitions enforce a typed schema across all environments. When you fetch configuration via the API, values are automatically cast and validated based on these types.
String
stringText values up to 255 characters. Default value is an empty string.
"site_name": "My Application"Number
numberNumeric values returned as floats. You can optionally set min, max, and step constraints. Default value is 0 (or the min value if set).
"max_upload_mb": 25Boolean
booleanTrue/false values. Default is false.
"maintenance_mode": falseDate
dateDate values with optional formatting. You can configure the output format using PHP date format strings. Supported formats include Y-m-d, d/m/Y, M j, Y, l, F j, Y, and Y-m-d H:i:s. Default is ISO 8601.
"launch_date": "2026-03-16"Array
arrayArrays of typed elements. Requires an item_type (string, number, or boolean). You can optionally define a set of allowed choices. Default is an empty array.
"allowed_origins": ["https://example.com", "https://app.example.com"]Webhooks
confish can notify your application when configuration values change by sending HTTP POST requests to a URL you configure per environment.
Setup
Add a webhook URL in the configuration settings - the gear icon on the Configuration tab. A signing secret is automatically generated when you first set a webhook URL.
You can find your webhook secret by navigating to your environment page and looking at the environment header - the secret is displayed alongside your API key and webhook URL. Use this secret to verify incoming webhook signatures.
By default, webhook payloads include the full configuration values. You can disable this with the Include values in payload toggle in environment settings - when disabled, webhooks act as a notification-only signal, and your application can fetch the latest values via the API.
Events
| Event | Description |
|---|---|
environment.updated | Configuration values were changed. Includes a changes array listing which fields were modified, and the full values unless Include values in payload is disabled. |
environment.deleted | The environment was deleted. Does not include values. |
Payload
When Include values in payload is enabled (the default):
{ "event": "environment.updated", "timestamp": "2026-04-05T12:00:00+00:00", "application": { "name": "My App" }, "environment": { "name": "Production", "env_id": "a1b2c3d4e5f6", "url": "https://confi.sh/c/a1b2c3d4e5f6" }, "changes": ["max_upload_mb"], "values": { "site_name": "My Application", "max_upload_mb": 25, "maintenance_mode": false }}The changes array lists which fields were modified in this update. When Include values in payload is disabled, the values field is omitted - use the API to fetch the latest configuration:
{ "event": "environment.updated", "timestamp": "2026-04-05T12:00:00+00:00", "application": { "name": "My App" }, "environment": { "name": "Production", "env_id": "a1b2c3d4e5f6", "url": "https://confi.sh/c/a1b2c3d4e5f6" }, "changes": ["max_upload_mb"]}Delivery & Retries
Webhooks are delivered asynchronously via a background queue. Your endpoint should respond with a 2xx status code as quickly as possible - ideally acknowledge the request first and do any heavy work afterwards. confish applies a 10-second request timeout per attempt.
Retry schedule
If your endpoint returns a 5xx status, a 429, or is unreachable, confish will retry the delivery up to 3 times with exponential backoff:
- First retry: 1 minute later
- Second retry: 5 minutes later
- Third retry: 15 minutes later
After three failed attempts (roughly 21 minutes later), the webhook is dropped and the failure is logged. Permanent client errors - any 4xx response other than 429 - are not retried, since they indicate the request will never succeed as-is (bad URL, rejected payload, etc.).
Idempotency
Because retries mean your endpoint may receive the same event more than once, every webhook request includes an X-Confish-Idempotency-Key header containing a UUID that stays the same across retries of the same delivery. You can use this key to safely de-duplicate events on your side:
X-Confish-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000A common pattern is to store processed keys in a short TTL cache (e.g. Redis) and skip any request whose key you've already seen within the retry window.
Signature Verification
Since your webhook endpoint is a publicly accessible URL, anyone could send a request to it pretending to be confish. Verifying signatures ensures that the webhook was actually sent by confish and hasn't been tampered with in transit. Without verification, an attacker could trigger actions in your system by sending forged payloads to your endpoint.
Every webhook request includes an X-Confish-Signature header that you can use to verify the request is authentic. The signature is an HMAC-SHA256 hash of the request body, prefixed with a timestamp.
Header format:
X-Confish-Signature: ts={unix_timestamp};sig={hmac_sha256_hash}To verify the signature:
- Extract the
tsandsigvalues from the header - Compute
HMAC-SHA256("{ts}:{body}", webhook_secret) - Compare the computed hash with
sig
Verify with the SDK
Each official SDK ships a one-liner verifier that handles parsing, timing-safe comparison, and the timestamp tolerance window. Pass the raw body, the header value, and your secret - on success it returns the parsed payload, so you never parse different bytes than you verified. On failure it throws a typed error telling you why: invalid signature, or timestamp outside the tolerance window.
import { verifyWebhook } from '@confish/sdk/webhook';
app.post('/webhook', express.text({ type: '*/*' }), async (req, res) => { let payload; try { payload = await verifyWebhook({ body: req.body, signature: req.headers['x-confish-signature'] as string, secret: process.env.CONFISH_WEBHOOK_SECRET!, }); } catch (err) { // Invalid signature or timestamp outside tolerance return res.status(401).send(err.message); }
// Handle the webhook event... res.sendStatus(200);});Logging
confish includes a simple logging API that lets you send log entries from your application and view them per environment in the dashboard. This is useful for tracking deployments, errors, or any event related to your configuration.
Sending a Log
curl -X POST https://confi.sh/c/a1b2c3d4e5f6/log \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"level": "info", "message": "User logged in"}'See Store Log Entry for the full reference and SDK examples.
Log Levels
debuginfonoticewarningerrorcriticalalertemergencyRetention
Logs are automatically pruned based on your plan. The Free plan retains up to 500 logs per environment for 14 days. The Pro plan retains up to 5,000 logs per environment for 30 days. When the limit is reached, the oldest logs are removed first.
Native logging adapters
Calling the SDK's log methods directly is the zero-dependency path. From 0.3.0, each SDK also implements its language's native logging interface, so the log calls you already make flow to confish with a config change - keep your existing logger, add confish as a sink. Adapters buffer entries in memory and flush in batches (at 50 entries or every 5 seconds), stamp each entry with the time it was logged rather than the time it was sent, and never raise into your code path - if delivery fails, entries are counted as dropped and your app keeps running.
handler, _ := confish.NewSlogHandler(client, confish.SlogHandlerOptions{})defer handler.Close()slog.SetDefault(slog.New(handler))
slog.Info("crawl finished", "pages", 12480)JavaScript has no adapter by design - the ecosystem has no consensus logging interface - so client.logs (including batch writes via writeBatch) is the JS story.
Actions
Actions let you send one-time commands from the dashboard to your applications. The consumer polls for pending actions, acknowledges them, reports progress, and marks them as completed or failed.
Lifecycle
Each action follows a lifecycle: pending → acknowledged → completed or failed. Actions that aren't acknowledged before their expiry time are automatically marked as expired.
List Pending Actions
/c/{env_id}/actionsReturns pending, non-expired actions ordered oldest first.
Acknowledge Action
/c/{env_id}/actions/{action_id}/ackCall this immediately when picking up an action to signal that processing has started.
Report Progress
/c/{env_id}/actions/{action_id}/updateAppend a timeline update. Each update has a message and optional data object.
messagestringrequiredA short description of what happened.
dataobjectOptional key-value pairs with details.
Complete or Fail
/c/{env_id}/actions/{action_id}/complete/c/{env_id}/actions/{action_id}/failBoth accept an optional result object for final output or error details.
Full Example
The raw curl flow drives the full lifecycle manually. Each SDK ships a consume() helper that polls, acknowledges, runs your handler, and reports the outcome - including idempotent skip on 409 and adaptive backoff when idle.
# 1. Poll for actionscurl https://confi.sh/c/a1b2c3d4e5f6/actions \ -H "Authorization: Bearer confish_sk_your_api_key"
# 2. Acknowledgecurl -X POST https://confi.sh/c/a1b2c3d4e5f6/actions/ACTION_ID/ack \ -H "Authorization: Bearer confish_sk_your_api_key"
# 3. Report progresscurl -X POST https://confi.sh/c/a1b2c3d4e5f6/actions/ACTION_ID/update \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"message": "Flushing cache", "data": {"keys": 512}}'
# 4. Completecurl -X POST https://confi.sh/c/a1b2c3d4e5f6/actions/ACTION_ID/complete \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"result": {"flushed": 512, "duration_ms": 84}}'Expiry
Actions expire after a configurable TTL (set when dispatching from the dashboard, default 1 hour, max 24 hours). Expired actions cannot be acknowledged, updated, or completed. Consumers should check for and skip expired actions.
Templates
If you find yourself dispatching the same action repeatedly, you can save it as a template from the dispatch sheet - tick Save as template, give it a name, and submit. Templates are application-wide, so the same template is available when dispatching from any of that application's environments.
Pick a template from the Load from template dropdown at the top of the sheet to hydrate the form with its type, parameters, and expiry - all fields stay editable, so templates act as defaults you can tweak before sending. Delete a template from the same dropdown using the trash icon next to its name; this doesn't affect any previously dispatched actions, since each action stores its own copy of the values it was sent with.
Feeds
Feeds are typed collections of living state you push into confish - scrape results, job runs, incidents, sensor readings. Define the fields once, pick a layout (cards, table, compact list, or hero), and the dashboard renders whatever you send. Items can be updated in place, deleted, or given a TTL so they clean themselves up. Hero renders each item big and centered - push one for singleton state like a quote of the day or today's draw, or a few and they stack as equally-weighted blocks in the sort order.
Definitions vs. items
A feed's definition (name, fields, layout) belongs to the application and is shared by every environment - like field definitions. Each environment holds its own items - like config values. The same code works against staging and production with only the API key swapped, and the two environments never see each other's data.
Feeds are created and configured in the dashboard (the Feeds tab on any environment). The API below is how you manage items. Feed writes only require a valid API key - the config write access toggle does not apply.
Upsert Item
/c/{env_id}/feeds/{feed_slug}/items/{external_id}Create or update an item. You supply the ID - use something natural to your domain (a fixture ID, an order ID, a listing URL hash). The same ID always updates the same item, so crash-and-retry loops are safe by default.
dataobjectrequiredThe item's values, validated against the feed's declared fields. Unknown fields are rejected with a 422 so schema drift in your code surfaces immediately.
ttlintegerSeconds until the item expires (max 30 days). Expired items disappear from all reads and are garbage-collected. PUT is declarative: omitting ttl on a later upsert makes the item permanent again.
curl -X PUT "https://confi.sh/c/a1b2c3d4e5f6/feeds/jobs/items/item-1" \ -H "Authorization: Bearer confish_sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"data": {"job":"example","pages":123,"status":"example"}, "ttl": 86400}'Returns 201 when the item was created, 200 when updated. If the environment's partition is at its plan cap, creates are rejected with a 422 - feeds hold living state, so nothing is silently pruned. Updates to existing items always succeed.
The feed helper is available in every official SDK.
Replace All Items
/c/{env_id}/feeds/{feed_slug}/itemsDeclaratively replace the environment's entire partition in one request: send {"items": [{"external_id", "data", "ttl"?}, ...]} and the feed becomes exactly those items - existing IDs update in place, new IDs are created, and anything absent is deleted. An empty list clears the feed. Built for sync-style jobs (a daily cron pushing its full dataset) - one request and one live update for viewers instead of hundreds. All-or-nothing: any invalid item rejects the whole request with a 422 and nothing written, and payloads over the plan's item cap are rejected the same way. Returns {"created", "updated", "deleted"} counts. In the SDKs this is feed.replace(items).
List Items
/c/{env_id}/feeds/{feed_slug}/itemsReturns the environment's live items, newest first. Expired items are never included.
Delete Item
/c/{env_id}/feeds/{feed_slug}/items/{external_id}Removes an item from the environment. Idempotent - deleting an item that is already gone returns 204, so retries never error. For items with a natural end (a finished run, a resolved incident), prefer setting a ttl on the final upsert over an explicit delete: the item lingers visibly for a while, then cleans itself up even if your script crashes first.
Sharing feeds publicly
Any feed can be published at a read-only public link - a zero-build live dashboard for whatever you publish. Sharing is per feed, per environment: you can make production's items public while staging stays private. Enable it from the feed's Share dialog on the environment's Feeds tab, where you can also give the page its own title. The link is unguessable and marked noindex; viewers see the feed's items and nothing else - no app, environment, or config details. Disable or regenerate the link at any time. Viewers can install a shared page as an app on their phone or desktop - each share carries its own web app manifest, named after the share. Public share links are a Pro feature; your code needs no changes.
Boards
A share publishes one feed; a board composes several - feeds from any of your applications, each section pinned to the environment of your choice, stacked in the order you set. Manage them from Boards in the sidebar: pick a title, add sections (each heading defaults to the feed name and can be renamed), and flip the public link on when you're ready - boards start as private drafts. Each section renders with its feed's own layout, so a hero section can headline the board above a table and a list. A board's public page carries the same guarantees as shares: unguessable /b/ link, noindex, live updates, installable as its own app, and nothing about your apps or environments is ever shown. Boards are a Pro feature.
Plans & Limits
confish offers a generous free tier for side projects and a Pro plan for higher limits.
| Feature | Free | Pro |
|---|---|---|
| Price | Free | £9.99/month |
| Applications | 3 | Unlimited |
| Environments per app | 3 | Unlimited |
| Action dispatches per month | 50 | Unlimited |
| API requests/min (per app) | 60 | 600 |
| Logs per environment | 500 | 5,000 |
| Log retention | 14 days | 30 days |
| Config history per environment | 10 changes | 100 changes |
| Feeds per application | 1 | 10 |
| Feed items (per feed, per environment) | 50 | 500 |
| Public feed share links | No | Yes |
| Boards | No | 5 |
| Webhooks | Yes | Yes |
Ready to get started?
Create your free account and have your configuration API running in minutes.