Forge API
Connect your AI agent to any machine. Clean, normalized, governed data in, supervised action out.
Quick start
- Get your API key: free at foundrynet.io/quickstart, issues a
forge_sandbox_key instantly, no card - Identify a machine:
POST /v1/identify - Send data:
POST /v1/normalize - Connect a tool:
POST /v1/tools - Set up automation:
POST /v1/triggers/natural - Query history:
GET /v1/history/{machine_id} - reproducible and verifiable against the corpus version record:
POST /v1/settle
All /v1/* requests need Authorization: Bearer <key> and (where applicable) Content-Type: application/json. There are two key tiers, and the same key behaves identically on REST and MCP:
forge_sandbox_…, free evaluation tier. 100 calls per hour across 3 machines, no card, $0 always.normalizeand the free tools work on it; the inference set (predict,predict_breach,fleet_health,diagnose,health_index) returns402until a payment method is added — that is billing, not a broken key. Get one at foundrynet.io/quickstart.fnet_…, metered API. Free tools work immediately; paid tools require a payment method on file.normalizemeters per normalized telemetry event; premium actions meter per action. Add a card at /pricing.
Agent keys (forge_agent_…) inherit the limits of the account that created them: an agent under a sandbox account gets the free allowance; under a paid account it is metered.
Limits
Two separate limits apply, and they return different errors:
- Rate — calls per hour for your tier (sandbox: 100/hour). Every response carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. Exceeding it returns429witherror: "rate_limit_exceeded", aretry_after_seconds, and the reset time. The window is a clock hour, so it frees up at the top of the hour. - Concurrency — 10 simultaneous in-flight
normalizecalls per key, on every tier. A burst of 20 parallel calls will see roughly half return429immediately withRetry-After: 1; they are not counted against your hourly quota and are safe to retry as soon as a call completes. Pipeline with a worker pool of 10 or fewer, or send multiple rows in one call —/v1/normalizeaccepts a CSV body, which is one call regardless of row count.
Normalize latency scales with how many of your tags are new to the corpus: unfamiliar tags take the vector and LLM paths on their first sighting and are cached afterwards, so the first call from a new plant is the slowest one it will ever make.
Endpoints
/v1/identify
Provision or look up a persistent, verified identity (machine_id) for one machine. Idempotent on (oem, model, serial).
{
"oem": "fanuc",
"model": "Robodrill α-D21MiB5",
"serial": "R30iB-12345",
"site": "Line 3"
}
{
"machine_id": "machine_4de03b",
"internal_id": "fanuc:m-20ia:r30ib-12345",
"created": true,
"machine": { "status": "active" },
"first_seen": "2026-05-04T01:00:00Z"
}
/v1/normalize
Translate raw OEM telemetry to canonical FCS data. Auto-provisions identity from oem/model/serial when no machine_id is given. Returns triggers_fired[] when any active trigger matches. Optional idempotency_key dedups retries within 24h. Optional observed_at (ISO 8601 controller clock) is stored alongside the server ingested_at and used for sustained-trigger windows.
{
"data": { "Spindle_Speed": 2200, "spindle_load": 92 },
"oem": "fanuc",
"model": "Robodrill α-D21MiB5",
"serial": "R30iB-12345",
"observed_at": "2026-05-05T09:30:15Z",
"idempotency_key": "optional-caller-supplied-key"
}
{
"normalized": { "spindle_speed_rpm": 2200, "spindle_load_pct": 92 },
"field_mappings": { "spindle_load": { "mapping_id": "6c34…", "canonical": "spindle_load_pct", "confidence": 0.93 } },
"fields_total": 2,
"fields_renamed": 2, // source != canonical
"fields_identity": 0, // source == canonical
"fields_unknown": 0, // no mapping
"coverage_pct": 100,
"machine_id": "machine_4de03b",
"history_id": "c419…",
"idempotency_key": "ab12…",
"observed_at": "2026-05-05T09:30:15+00:00",
"ingested_at": "2026-05-05T09:30:18+00:00",
"deduplicated": false,
"triggers_fired": [{ "trigger_id": "7f0c…", "trigger_name": "high spindle load", "actual_value": 92 }]
}
If a retry hits the same idempotency_key within 24h, response includes deduplicated: true + the original history_id, and trigger evaluation is skipped. If |observed_at − ingested_at| > 60s, response includes clock_skew_warning.
/v1/tools
Register a webhook endpoint as a tool. Triggers reference these by tool_id. auth_secret returned ONCE on create; never read back via GET.
{
"name": "slack-maintenance",
"url": "https://hooks.slack.com/services/T…/B…/xxx",
"method": "POST",
"payload_template": { "text": "Alert: {{oem}} {{model}}, {{field}} at {{value}}" },
"auth_type": "none"
}
{ "id": "add2…", "name": "slack-maintenance", "auth_type": "none", "enabled": true }
/v1/triggers/natural
Parse a natural-language instruction into a structured trigger. NEVER auto-activates; returns parsed_trigger for review and explicit POST to /v1/triggers.
{ "machine_id": "machine_4de03b", "instruction": "Alert maintenance Slack when spindle load exceeds 90." }
{
"parsed_trigger": {
"machine_id": "machine_4de03b",
"name": "Spindle Load Alert",
"condition": { "field": "spindle_load_pct", "op": ">", "value": 90 },
"actions": [{ "tool_id": "add2…" }]
},
"confirmation_required": true
}
/v1/history/{machine_id}
Per-machine canonical history. Query params: from, to (ISO-8601), fields (csv), limit (≤1000), summary (bool).
GET /v1/history/machine_4de03b?limit=5&summary=true
{
"machine_id": "machine_4de03b",
"machine": { "oem": "fanuc", "model": "Robodrill α-D21MiB5", "site": "Line 3" },
"row_count": 2,
"avg_coverage": 100,
"fields_covered": ["spindle_load_pct", "spindle_speed_rpm", "sensor_readings.coolant_temp"]
}
/v1/settle
Create a reproducible and verifiable against the corpus version, independently auditable record. Every event is hash-chained into the machine's verified history. Add ?batch=true with body {"machine_id": "…"} to roll all unsettled events for that machine into one record.
POST /v1/settle?batch=true
{ "machine_id": "machine_4de03b" }
{
"machine_id": "machine_4de03b",
"merkle_root": "d35ae2e…",
"event_count": 4,
"event_types": { "identify": 1, "normalize": 3 },
"period_start": "2026-08-02T21:00:00Z",
"period_end": "2026-08-02T22:00:00Z",
"settlement": "anchored",
"settled_at": "2026-08-02T22:00:04Z"
}
Read the machine's verified history back with GET /v1/history/{machine_id}.
The verified history is exportable for lessors, insurers, and auditors. Optional off-network anchoring is available on Enterprise.
MCP server
Connect any MCP-compatible client (Claude, Cursor, custom agents, LangChain, CrewAI, AutoGen). One line:
claude mcp add --transport http forge https://mcp.foundrynet.io/mcp --header 'Authorization: Bearer YOUR_API_KEY'
Endpoint: https://mcp.foundrynet.io/mcp (Streamable HTTP). Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"forge": {
"command": "npx",
"args": ["mcp-remote", "https://mcp.foundrynet.io/mcp"]
}
}
}
Listed on Smithery, Glama, and the MCP directory.
MCP tools (30 total)
identify_machine, provision/lookup machine_id from (oem, model, serial)normalize_telemetry, raw OEM data → canonical FCS, with auto-provision + trigger evalquery_machine_history, time-series + summary over canonical_datacreate_automation, natural language → parsed_trigger (review-then-activate)activate_automation, arm a parsed_trigger after user confirmationlist_automations, show all triggers configured for a machinedisable_automation, pause a trigger without deleting it (reversible)delete_automation, soft delete (30-day restorable);?permanent=truefor hard deletequery_webhook_history, webhook delivery history for a trigger (HTTP status codes + timing)restore_automation, undo a soft delete within 30 daysverify_record, create a reproducible and verifiable against the corpus version record (single payload OR batched)fire_sandbox, demo the full watch→fire→verify loop against a built-in sandbox machinecorrect_mapping, teach Forge the right canonical field for a source columnget_coverage, ask what Forge can normalize before you send data (recognized OEMs + fields)predict, forecast the next N readings of a canonical telemetry seriespredict_breach, predict whether and when a series will cross a thresholdremaining_life, estimate remaining useful life before a failure thresholdpredict_batch, run predictions for an entire fleet in one callfleet_health, roll a fleet of predictions into one health dashboarddetect_anomalies, flag anomalies without a full forecast (z-score + IQR)machine_intelligence, normalize + predict + assess in one call from raw telemetryprediction_accuracy, report how well past predictions matched reality
Plus operational capability tools, calculate_oee, fleet_oee, energy_consumption, shift_report, health_index, diagnose_machine, get_agent_card, list_agents, list_guardrails, and check_guardrail, for 32 tools total. All run on the same normalized data layer. Works with any MCP-compatible client.
Prediction input contract
The forecasting tools are stateless. They do not read your stored telemetry. Passing only a machine_id returns 422. You send the series you want forecast, in the request body, on every call. Forge stores your normalized history for health, OEE, and triggers, but a prediction never reads it, so you always know exactly which numbers a forecast was based on.
The forecasting tools, predict, predict_breach, and remaining_life, take the canonical series as a time_series array (not values), ordered oldest to newest, and require a minimum of 16 numeric data points. The canonical_field is optional; when supplied it must be a recognized canonical, call get_coverage to list them. machine_id is the machine's identifier; on a prediction it only labels the reproducible and verifiable against the corpus version integrity record and does not load data.
POST /v1/predict_breach
{
"canonical_field": "spindle_load_pct",
"time_series": [60, 61, 62, 63, 64, 66, 67, 68, 70, 71, 73, 74, 76, 78, 80, 82],
"threshold": 95,
"direction": "above",
"horizon": 24
}
A real 16-point series read off a Prusa MK2 hotend as it cools, showing the same contract with live data:
curl -X POST https://forge.foundrynet.io/v1/predict_breach \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"canonical_field": "hotend_temperature_c",
"time_series": [167.1, 162.0, 159.0, 155.8, 153.4, 150.7, 148.3, 146.0,
143.2, 140.8, 138.2, 136.1, 134.0, 131.8, 129.7, 127.9],
"threshold": 100,
"direction": "below",
"horizon": 16
}'
fleet_health and predict_batch take a fleet payload and are POST, not GET. fleet_health expects machines (each carrying its own values), not machine_ids, and is likewise stateless. Fewer than 16 points, an all-identical series (no variance), or an unrecognized canonical field returns the 422 envelope shown below.
Reliability + record integrity
Soft delete
DELETE /v1/triggers/{id} defaults to soft delete, sets deleted_at, hides from list/eval, restorable for 30 days via PATCH /v1/triggers/{id}/restore. Add ?permanent=true for irrecoverable hard delete.
Idempotency
/v1/normalize accepts an optional idempotency_key. If absent, the server synthesizes sha256(machine_id + canonical(data))[:32]. Repeats within 24h return the original response with deduplicated: true and skip trigger evaluation. Telemetry gateways: set this on retry.
Coverage metric
Every /v1/normalize response carries three counters: fields_renamed (source != canonical), fields_identity (source == canonical), fields_unknown (no mapping). coverage_pct = (renamed + identity) / total. Identity mappings, like parts_count → parts_count at confidence 1.0, count toward coverage.
Record timestamps
observed_at (ISO 8601 from the controller clock) is optional on /v1/normalize. Stored alongside ingested_at (server). When the difference exceeds 60s, the response includes clock_skew_warning. Sustained-condition trigger windows reference observed_at when present so windows are anchored to when events actually happened, not when they arrived.
Structured errors
Every /v1/* validation failure returns the same envelope:
{
"error": true,
"status": 422,
"field": "body.oem",
"reason": "Field required",
"received_value": {},
"suggestion": "e.g. 'fanuc'",
"additional_errors": [ { "field": "body.model", "reason": "Field required" } ]
}
System requirements
The Forge kernel runs as a Docker container on any Linux host: cloud, on-premise server, industrial PC, or edge gateway.
Minimum (edge deployment)
CPU: 2 cores, x86_64 or ARM64
RAM: 4 GB
Storage: 10 GB available
OS: Linux (Ubuntu 20.04+, Debian 11+, Amazon Linux 2)
Network: Ethernet to OT network (Modbus TCP / OPC UA / MTConnect / EtherNet/IP / PROFINET / BACnet / ROS2 / J1939 / MAVLink)
Optional internet for cloud sync and updates
Runtime: Docker 20.10+ or AWS Greengrass V2
Recommended (production)
CPU: 4 cores
RAM: 8 GB
Storage: 50 GB (accommodates extended telemetry history)
Network: 1 Gbps to OT network + internet for CMMS and updates
Cloud deployment
Any container host: Railway, AWS ECS, GCP Cloud Run, Azure Container Apps. No special hardware. Connects to equipment via API or protocol bridge.
Resource profile
Container image: ~2.2 GB (compressed ~900 MB)
Runtime memory: ~1.1 GB RSS steady state, ~1.5 GB peak
CPU at idle: < 5% single core
CPU during predict: ~40% single core for 2-3 seconds
Bandwidth: < 1 MB/hour per machine at 1 reading/second
What is in the 1.1 GB
Normalization engine (BGE-M3 embeddings): ~600 MB
Prediction engine (TimesFM): ~400 MB
Application + vertical packs + schema: ~100 MB
Supported protocols
13 usable of 15 declared. This list is the same set GET /health reports, so the two cannot disagree; the unavailable two are named below with their blockers rather than omitted.
MQTT / Sparkplug B (push, real-time streaming — see the note below)
Modbus TCP (poll, legacy PLCs and sensors)
OPC UA (subscribe, modern controllers)
MTConnect (poll, CNC controllers: Haas, Mazak, Okuma, DMG Mori)
EtherNet/IP (CIP) (poll, Allen-Bradley / Rockwell PLCs)
PROFINET / S7 (poll, Siemens S7-300/400/1200/1500)
BACnet/IP (poll, building automation: BMS, HVAC, meters)
ROS2 / DDS (subscribe, robots and AMR fleets)
J1939 / CAN (listen, engines and heavy equipment)
MAVLink (listen, drones and autonomous vehicles)
MachineMetrics (poll, GraphQL — machines already on MachineMetrics)
REST API (push or pull, any HTTP-capable device)
CSV upload (batch, historical data import)
Declared but not available:
Modbus RTU no serial transport is implemented; use Modbus TCP,
or a serial-to-TCP gateway in front of the device
SNMP the adapter is not wired
Sparkplug B, stated precisely: the transport works and topics are parsed, but Sparkplug's protobuf payloads are not decoded, so a device that publishes metrics only inside a protobuf body will connect and produce nothing usable. Plain MQTT payloads are unaffected.
No new hardware required
Forge connects to your existing sensors, PLCs, and controllers through the protocols they already speak. No proprietary gateways, no sensor replacements, no wiring changes.
Pricing
Metered billing via Stripe. Counts roll up to your subscription's current billing period and settle on Stripe's normal cycle. Query GET /v1/usage for live counts and an estimated bill.
A Normalized Telemetry Event (NTE) is each individual tag-value pair submitted for resolution — a single request carrying N tag-value pairs is N NTEs. Normalize bills on graduated tiers per 1,000 NTE: the first 100M events each month bill at the first-tier rate no matter your total volume, and only the events above each threshold bill at the next rate. Full ladder at /pricing.
POST /v1/normalize, $0.30 per 1,000 NTE, graduating to $0.06 per 1,000 above 50B/month- Trigger fire (per webhook delivery, only on 2xx), 5¢
POST /v1/settle?batch=true, 2¢ per batch verified (skipped verifications are free)GET /v1/history/{machine_id}, freePOST /v1/identify, freePOST /v1/triggers/natural, free (setup step; revenue comes from the trigger fires it produces)POST /v1/feedback/{mapping_id}/confirmand/correct, free- Trigger evaluation (the eval pass during
/v1/normalize), free; you only pay for fires that successfully deliver
Get an API key via POST /v1/keys with a Stripe payment method (email + payment_method_id from Stripe.js).