The API
JSON over HTTPS on the company's own subdomain, authenticated with an API key as Authorization: Bearer fdr_…. A key's scope limits it: read keys may only GET, write keys may do everything except approve or send a purchase order, approve keys may do those too. Decimals (quantities, money) are strings — JSON numbers are floats and these are not. Errors are {"error": "…"} with a meaningful status.
Reading
| Endpoint | Returns |
|---|---|
GET /api/parts · /api/parts/{id} |
Parts, paged (?page=&per=); ?category=504 narrows to a category, ?q= searches number, name and description; a part includes on_hand |
GET /api/parts/{id}/bom |
The part's BOM lines: child part, quantity per, reference, routing step |
GET /api/parts/{id}/routing |
The part's routing steps, with totals |
GET /api/parts/{id}/images |
The part's images: id, filename, primary flag |
GET /api/suppliers |
The suppliers |
GET /api/parts/{id}/sourcing |
Who sells the part, as what, for how much |
GET /api/demand?status=open&channel= |
Demand orders with channel, reference, quantity and shipped quantity |
GET /api/channels |
The channels |
GET /api/channels/{code}/listings |
Every part listed on the channel with its channel part number; null means not pushed yet |
GET /api/stock |
One row per part per location |
GET /api/sourcing?supplier=&supplier_part_number=&part_number= |
Which of our parts is a supplier's number, or the reverse. Case-insensitive |
GET /api/purchase-orders?status=&number=PO-00004 |
Orders with supplier, subtotal and a link. number finds one the way people name it |
GET /api/purchase-orders/{id} |
An order with its lines |
GET /api/workstations · /api/workstations/{id} |
Stations with group and status |
GET /api/workstations/{id}/next |
The job this station should do now, with its materials — null unless the station is idle and a step is ready. ?material=<part id> restricts to jobs consuming that part. The response carries the station's current_material and requires_material_change |
GET /api/work-orders?status=&number=WO-00023&external_ref= · /api/work-orders/{id} |
Orders; a single order includes its steps and each step's materials and readiness |
GET /api/{parts,workstations,work-orders}/{id}/custom |
The opaque JSON, or null |
Writing
| Endpoint | Body | Does |
|---|---|---|
POST /api/purchase-orders |
{supplier, currency?, exchange_rate?, expected_at?, external_ref?, notes?, lines:[{supplier_part_number | part_id, quantity, unit_price?}]} |
Creates a draft; lines by their number are resolved through sourcing. An unknown number fails the whole request naming the line. A human approves |
PUT /api/workstations/{id}/status |
{"status": "idle|busy|blocked|offline", "note"?} |
Mirrors a machine's state |
POST /api/work-order-steps/{id}/start |
{"workstation_id"?} |
Takes a step |
POST /api/work-order-steps/{id}/complete |
{"quantity": "9", "consumed"?: [{"part_id", "quantity"}]} |
Records units made; backflushes materials at BOM standard, or at the reported actual for any part named in consumed; last step receives output |
POST /api/work-order-steps/{id}/scrap |
{"quantity": "9", "consumed"?: […], "note"?} |
A run that made nothing: materials consumed, no output, the step's count unchanged |
POST /api/work-order-steps/{id}/pause |
A running step with quantity left goes back to pending, progress kept, station released — so the next job is chosen afresh. The print manager does this after every bed | |
PUT /api/workstations/{id}/material |
{"part_id": 934} or {"part_id": null} |
What the station is set up with — a printer's loaded filament |
PUT /api/tasks/{source} |
[{key, priority, message, href, actions?: [{key, label, tone?}]}] |
Replaces this source's task set wholesale; anything omitted is resolved. Source names are lowercase slugs. A task may offer up to six actions — buttons a human can press; pressing one resolves the task with that key as its outcome, which the source reads back. A task with an outcome is never reopened: use a new key for a new occurrence |
GET /api/tasks/{source} |
This source's open tasks and its recently resolved ones with an outcome — how an integration learns which button was pressed |
|
PUT /api/{kind}/{id}/custom |
The JSON itself | Replaces the opaque blob (64 KB max) |
POST /api/parts |
{category_code, name, part_type, description?, notes?, unit_of_measure?, is_stocked?, minimum_stock?, lead_time_days?, custom?} |
Creates a part; the stock number is allocated in the category. Returns the part |
POST /api/parts/{id}/bom/lines |
{child_part_id, quantity, reference?, notes?, routing_step_id?} |
Adds a BOM line; the cycle rules apply |
PUT /api/bom/lines/{id} |
{quantity?, reference?, notes?} |
Edits a line |
POST /api/parts/{id}/routing/steps |
{name, group_code?, seq?, setup_minutes?, run_minutes_per_unit?, batch_size?, instructions?} |
Adds a routing step; seq blank takes the next ten |
POST /api/parts/{id}/images |
multipart, field file |
Uploads an image (12 MB max); the first becomes primary |
PUT /api/parts/{id}/sourcing/{supplier} |
{supplier_part_number, unit_price?, currency?, product_url?, minimum_order_qty?, pack_size?, lead_time_days?, is_preferred?, notes?} |
Creates the supplier's row for the part (201) or updates it (200); fields not sent keep their value. A part's first source becomes preferred |
POST /api/demand |
{channel, reference, part_id | part_number | channel_part_number, quantity, due_at?, kind?, notes?} |
Demand received. Idempotent on (channel, reference): a repeat while open updates quantity and date and answers 200; a first arrival answers 201. A closed or cancelled reference is left alone |
POST /api/demand/{id}/ship |
{quantity?, location_id? | location_code?} |
Demand shipped. Issues that quantity (blank: the rest) from the location, or the part's biggest pile, as a ledger movement referencing the demand; closes the demand once everything has gone. 409 when there is not enough on hand |
POST /api/demand/{id}/cancel |
Demand cancelled | |
PUT /api/channels/{code}/listings/{part_id} |
{channel_part_number?, is_listed?, notes?} |
Lists a part on the channel, or records the channel's number for it |
To be told when something happens rather than asking, see webhooks.
For an assistant
What a chat agent needs to keep a person on track: one call for "what needs me", lookups by the names people use, and the buttons. Every item carries a url a human can click.
| Endpoint | Returns |
|---|---|
GET /api/brief |
Everything that needs attention now: open tasks, workstation states, draft and open work orders, purchase orders awaiting approval, approved but unsent, and overdue, planned orders due to be firmed, transfers needed, shortages, and jobs the capacity check says will finish late. headline says the same in a few sentences, most urgent first |
GET /api/tasks |
Every open task, most urgent first, with its actions |
GET /api/planning |
The planning page: proposed and firmed planned orders (each says because of what), and each workstation group's schedule with its late jobs |
GET /api/demand/{id}/progress |
"Where is my order?": the demand, the part's stock and inbound supply, the planning run's proposals for this order, the work orders making it, and which of those are scheduled late. Supply belongs to the part, not the order: two orders for one part see the same stock |
Doing things
| Endpoint | Body | Needs | Does |
|---|---|---|---|
POST /api/tasks/{id}/act |
{"action": "ready"} |
operator | Presses one of the task's buttons |
POST /api/tasks/{id}/snooze |
{"days": 2} |
operator | Hides the task; a week when no number is given |
POST /api/tasks/{id}/resolve |
operator | Closes the task by hand | |
POST /api/work-orders |
{part_number | part_id, location, quantity?, external_ref?, due_at?, priority?, notes?, output_location?, release?} |
planner | Raises a work order: a draft, or released when release is true. location is the code of where it draws parts (and puts output, unless output_location differs). external_ref is your name for what the job is about. Send an Idempotency-Key, or a retry raises a second order |
POST /api/work-orders/{id}/issue |
{part_number | part_id, quantity?, note?} |
operator | Draws a part from stores for the order, outside its BOM. 409 when there is not enough |
POST /api/work-orders/{id}/release |
planner | Draft to released. Answers with the order and each step's readiness | |
POST /api/work-orders/{id}/cancel |
planner | Cancels an unfinished order | |
POST /api/planning/run |
planner | Runs planning now. Answers with the proposals | |
POST /api/planning/firm |
{"ids": [12, 13], "location": "BHILL"} |
planner | Proposals into draft work orders and purchase orders. location (or location_id) is needed when a make is among them |
POST /api/purchase-orders/{id}/cancel |
planner | Cancels the order | |
POST /api/purchase-orders/{id}/approve |
admin, approve key |
Draft to approved | |
POST /api/purchase-orders/{id}/send |
planner, approve key |
Records the order as sent. Foudra does not transmit it to the supplier |
Four rules apply to every one of these:
- Who. Send
X-On-Behalf-Of: someone@example.comand the change is made in that user's name, with that user's role. Without it, the key's creator is the actor. Both must hold the role: a key cannot lend its creator's authority to someone else, nor borrow more than its creator has. The header works on the read endpoints too, so the brief is that user's brief. - Preview. Add
?dry_run=1and the request does everything for real and then undoes it. The answer is exactly what would have happened, with"dry_run": true; nothing is changed. Use it before asking a human "shall I?". - Once. Send an
Idempotency-Keyheader (any string up to 200 characters, unique per intended action). A repeat within a week gets the first answer back, with the headerIdempotent-Replay: true, and nothing is done twice. The same key on a different request is refused (422). - Record. Each change writes an audit row naming the key and, when given, who it acted for.
Every answer has summary, a sentence fit to repeat to a human, data, and acted_as. Refusals: 403 for a key's scope or a user's role, 404, 409 when the thing is not in a state that allows it (the message says why), 422 for a bad action or a reused key.
The print-farm shape
Poll /next?material=<loaded> while idle — anything ready in what is loaded? If not, /next unfiltered says what the top job needs; push a change filament task and go blocked. The human fits the spool, sets Loaded material on the workstation page, taps Ready. Then start; print; complete with the quantity and the actual filament used; status idle again. A failed print is scrap, not a completion of zero. The MRP knows nothing about printers.
API writes are recorded against the user who created the key, unless X-On-Behalf-Of names someone else (see above).