Notable changes in recent IoTMan releases.
AsyncAPI export and import, per-source schema validation, BYO workspace SMTP, third-party OTP sign-in.
Each workspace now publishes a single AsyncAPI 3.0 document covering its HTTP sources, MQTT sources, export endpoints, and webhook subscriptions, plus tag rules and transformation outputs under x-iotman-* extensions. Download asyncapi.yaml or asyncapi.json from the new AsyncAPI panel in workspace settings, edit in your tool of choice, then import with a dry-run diff before applying. An Admin-only Public toggle exposes the document without authentication so external tooling can fetch it directly.
Every HTTP endpoint row in Connect has a Schema button. Paste a JSON Schema, pick a validation mode (off, warn, or reject), and ingest checks each payload against it. Reject returns 422; warn accepts the payload but echoes the violations back to the producer in the response body and an X-IotMan-Schema-Violations header, so a producer can see and fix its own bug without an operator opening the dashboard. MQTT ingest honors the same schema and modes.
A new Workspace SMTP card in workspace settings stores per-workspace SMTP credentials and routes that workspace's OTP and invitation emails through them. Passwords are stored encrypted and never returned from the dashboard. A Send test email panel validates credentials before you commit. When no workspace SMTP is configured, sends fall back to the platform mailer. An Email deliveries card below the form shows the last 50 sends with status and error inline, refreshing automatically after each test.
The Invite member form on workspace settings sends an email with an accept link, lists pending invitations with a cancel button, and routes the invitee through the login flow if they aren't signed in yet. Accept is bound to the email the invitation was issued to; signing in as a different account returns a clear error.
A new OAuth2 grant lets third-party apps authenticate users against IoTMan without redirecting the browser to iotman.io. POST /api/auth/send-otp with the app's client_id mails a 6-digit code (the email names the requesting app); the app then exchanges the code at /oauth2/token using grant_type=urn:iotman:params:oauth:grant-type:otp. The code is bound to the calling client at issuance, so it cannot be redeemed at another client. /api/auth/send-otp accepts cross-origin requests; the rest of /api/auth/* stays same-origin.
The workspace identifier in every URL is now the slug: /api/workspaces/{slug}/..., /data/{slug}/{url-path}, and /mcp/{slug}. The OAuth2 aud claim and TokenInfoResponse.workspace_id now also carry the slug. Existing slugs are preserved verbatim, so existing devices, AsyncAPI consumers, and bookmarks continue to work. Access tokens minted before the cutover are invalidated and need to be refreshed; refresh tokens, cookie sessions, and dashboard logins are unaffected.
The per-workspace MCP catalog now ships with instructions pointing agents at the AsyncAPI bulk-config path so they can read or rewrite the workspace in one call instead of stepping through dozens of CRUD tools. A ?mode=lean query parameter trims the catalog to GET-only tools plus the AsyncAPI tools. Workspace settings shows the MCP endpoint URL with a copy button. Hosted MCP clients (ChatGPT, web Claude) that register through Dynamic Client Registration can now complete sign-in against their own callback URL instead of being forced onto loopback.
The OpenAPI spec is now served at /openapi.yaml. /llms.txt indexes the marketing and docs pages for AI agents; /llms-full.txt concatenates the full docs corpus in one fetch. sitemap.xml and robots.txt cover search crawlers. Open Graph and JSON-LD metadata are set on every page so link previews resolve consistently.
iotman.io now leads with a managed-backend framing aimed at small product teams; the IoT-first landing moves to /solutions/iot and remains linked from the nav and footer. The dashboard demo gets its own slot in the nav next to Login.
Raw and Processed endpoints unified; constructors gain $received_at and $metadata; MCP live in prod.
The split between Raw and Processed endpoints is gone. Toggling Read access on an HTTP endpoint in Connect now creates a Processed export endpoint pointing at a passthrough transformation, so REST, WebSocket, and webhooks all work uniformly on every endpoint listed under Export.
Two real PostgreSQL JSONPath named variables are bound on every constructor evaluation. $received_at is the row's ingestion timestamp; $metadata is the per-row ingest metadata (source_ip, auth_method, oauth2_client_id, ingest_context, …) and is fully subscriptable. Use them as values, in subscriptions like $metadata.source_ip, or inside filter expressions: $.value ? ($metadata.auth_method == "oauth2").
Saving an output now runs the constructor through a synthetic-row evaluation before persisting. Typos like $recieved_at fail at save time with a 400, instead of silently emitting nulls when real rows are processed.
Because every endpoint is now Processed, the Webhooks block in the expanded Export-tab row renders for every endpoint — including the ones materialised by the Connect-tab Read-access toggle.
The MCP endpoint announced in v0.5.0 is now actually serving traffic. /mcp/{workspace-uuid} and the OAuth2 discovery endpoints under /.well-known/ respond instead of returning 502.
The OAuth2 consent step is now a plain "Sign in to {App} as {email}?" prompt with a "Use a different account" escape hatch. Scope strings are gone from the page — admins decide what an end-user can do, so listing them on consent informed nobody.
Internal API Bearer requests now check the principal's iotman: scopes against the database on every request. Granting or revoking a scope from the Access tab no longer waits for the access-token TTL to roll over.
Webhook signing secrets are shown in full on every read, not just on creation. The Secret column on the Webhooks block has a copy button and survives lost dashboard sessions; you no longer have to re-create a webhook to recover its secret.
Authorization-code tokens issued to third-party clients now always include the iotman: scopes the user was granted, instead of being narrowed away by the client's requested-scope list. Matches how refresh-token rotation already worked.
Webhooks, MCP server, programmatic dashboard access, per-workspace data retention.
Each Processed export endpoint accepts webhook subscriptions. Deliveries are signed with HMAC-SHA256 over a timestamp and body, retried on a 1m / 5m / 30m / 2h / 12h schedule, and deduped on event id. The signing secret is shown once on creation; the dashboard shows last-delivery status and circuit state per subscription.
Processed export endpoints now accept a WebSocket connection in addition to REST GET. Frames carry the transformed payload — the same shape as one item of the polled data array and the webhook POST body. Live-only; REST covers backfill.
Each workspace exposes an MCP endpoint at /mcp/{workspace-uuid}. Every iotman API operation is available as an MCP tool. Authentication uses your existing OAuth2 client. Claude Code, MCP Inspector, and other spec-compliant clients can register dynamically and obtain a token via PKCE.
Internal API routes under /api/workspaces/{id}/... now accept OAuth2 Bearer tokens in addition to the dashboard session cookie. PKCE end-users and Client Credentials tokens can manage workspaces directly. Scopes are granted per email (end-users) or per client (Client Credentials) from the Access tab.
Internal API permissions now use a per-resource, per-verb scope namespace (e.g. iotman:endpoints:create, iotman:webhooks:read). Built-in Admin / Editor / Viewer roles expand to the equivalent granular sets. The Access tab exposes Read-only / Operator / Full access preset chips for one-click bundles, with per-scope add and revoke for fine-tuning.
Each workspace now has a retention setting in days. Records older than the cutoff are dropped automatically. Leave it unset to keep data forever.
The full OpenAPI spec is published at /openapi.yaml and rendered as a browsable reference at /api-reference.
Tag categories and dynamic rules are now one concept. The Process tab edits both the rule fields and the tag metadata in a single form. The /tag-categories and /dynamic-rules API paths are removed; use /tag-rules.
The token-based /data/{endpoint_token} ingest, read, and stream URLs and the UUID-keyed /data/out/{id} export URL are gone. Use the workspace-scoped /data/{workspace-short-id}/{url-path} URL shown on the Connect and Export tabs. The Deprecation headers shipped in v0.4.0 already pointed at the replacement.
Ingestion auth reworked, workspace-scoped export URLs, WebSocket streaming.
Output endpoints are now classified as Raw or Processed. Raw endpoints deliver each record exactly as it was ingested — same URL as the input, only the method differs. Processed endpoints run a constructor to shape the payload. Raw skips the transformation step, so polled reads are cheaper and WebSocket streaming is available. WebSocket is not yet available on Processed endpoints.
Export endpoints are served at GET /data/{workspace-short-id}/{url-path}. Both segments are set per workspace and per endpoint.
Raw endpoints accept a WebSocket connection and push new records as they are ingested. Each endpoint's stream is scoped to its own source; earlier releases sent every workspace event to every stream subscriber.
Protected streams authenticate via a first-message handshake instead of a query-string token. The access token no longer appears in URLs or server logs.
Basic-auth and mTLS are removed. Each ingestion endpoint is either open or protected by OAuth2; protected endpoints validate a Bearer access token against a required scope.
GET /oauth2/userinfo returns the standard OIDC claims. The previous introspection endpoint is removed.
Each stored record now carries a metadata object captured at ingest time (such as the token that wrote it), queryable alongside the payload.
Dashboard sessions now last 30 days. Sign-in is still passwordless.
A privacy page is now published at /privacy, listing cookies and the legal basis for processing.
Notable changes are now summarised here on every release. v0.4.0 is the first entry; earlier releases are not backfilled.