API & Integrations
Everything in GarmentFlow is reachable over HTTP. The Desk, the floor apps and the dashboards all talk to the same endpoints you can call — there is no private interface they use and you don't.
That means you can read production data into Power BI, push an order in from your webshop, or build your own phone app, without anyone writing a special integration for you.
This page starts from nothing and works up to a working mobile app.
The two kinds of endpoint
Resource API — one endpoint per DocType, generated automatically. Create, read, update and delete records.
/api/resource/<DocType>
Method API — one endpoint per whitelisted server function. Returns whatever that function returns, usually something already assembled for a screen.
/api/method/<full.dotted.path.to.function>
Rule of thumb: resource to read or write records; method when GarmentFlow already computes what you want.
Step 1 — Create an integration user
Do not use your own login. Create a user that exists only for the integration, so you can see what it did and switch it off without locking anyone out.
- Awesome Bar (Cmd/Ctrl + K) →
user→ + Add User. - Email
integration.powerbi@yourfactory.com, first namePower BI Integration. - User Type:
System User. - Under Roles, give it only what it needs. For a read-only reporting user, that is typically
Manufacturing User— notSystem Manager. - Save.
The API enforces exactly the same permissions as the interface. A user who cannot open a Production Order in Desk gets a 403 for it over the API. This is your security boundary — get the roles right and the integration cannot exceed them, whatever the code does.
Step 2 — Generate API keys
- Open the user.
- Scroll to Settings → API Access.
- Click Generate Keys.
- The API Secret is shown once. Copy it now.
- The API Key stays visible on the user record.
You now have a pair:
API Key: a1b2c3d4e5f6g7h
API Secret: s9t8u7v6w5x4y3z
Every request carries them in a header:
Authorization: token a1b2c3d4e5f6g7h:s9t8u7v6w5x4y3z
Note the format: the word token, a space, then key and secret separated by a colon.
Never put an API secret in a mobile app or a web page. Anyone can extract it. A phone app should log its user in and use a session, or talk to a small backend of yours that holds the secret. See Securing what you build.
Step 3 — Make your first call
Test with curl before writing any code — it isolates whether the problem is the API or your program:
curl -X GET \
'https://your-site.com/api/resource/Production%20Order?limit_page_length=5' \
-H 'Authorization: token a1b2c3d4e5f6g7h:s9t8u7v6w5x4y3z'
A successful response:
{
"data": [
{ "name": "PO-2026-0042" },
{ "name": "PO-2026-0041" }
]
}
Note %20 for the space in Production Order. Most HTTP libraries do this for you.
If this works and your program doesn't, the difference is in your program. If this fails, fix it here first.
Reading records
A list
GET /api/resource/Production Order
By default you get names only. Ask for fields:
GET /api/resource/Production Order?fields=["name","status","end_date","total_planned_qty"]
Filtering
Filters are JSON, a list of [fieldname, operator, value]:
?filters=[["status","=","In Progress"]]
?filters=[["status","in",["Released","In Progress"]],["end_date","<","2026-09-01"]]
Operators: =, !=, >, <, >=, <=, in, not in, like, between, is (with set / not set).
Sorting and paging
?order_by=end_date asc
?limit_start=0&limit_page_length=50
limit_page_length=0 returns everything — avoid it on large tables. Page instead: limit_start=0, then 50, then 100.
A complete read:
GET /api/resource/Production Order
?fields=["name","status","end_date","total_planned_qty"]
&filters=[["status","in",["Released","In Progress"]]]
&order_by=end_date asc
&limit_page_length=50
One record, in full
GET /api/resource/Production Order/PO-2026-0042
This returns every field including child tables — the operation plan, the items, the materials summary. The list endpoint never returns child tables; fetch the document when you need them.
Writing records
Create
curl -X POST 'https://your-site.com/api/resource/Fabric Test Certificate' \
-H 'Authorization: token KEY:SECRET' \
-H 'Content-Type: application/json' \
-d '{
"tech_pack": "TP-0031",
"test_date": "2026-08-20",
"laboratory": "Textil Lab SA",
"result": "Pass"
}'
The response is the created document, including its generated name.
Update
curl -X PUT 'https://your-site.com/api/resource/Fabric Test Certificate/FTC-2026-0007' \
-H 'Authorization: token KEY:SECRET' \
-H 'Content-Type: application/json' \
-d '{"result": "Fail"}'
Send only the fields you are changing.
Submit and cancel
These are not field edits — they are transitions. Set docstatus:
curl -X PUT 'https://your-site.com/api/resource/Production Order/PO-2026-0042' \
-H 'Authorization: token KEY:SECRET' \
-H 'Content-Type: application/json' \
-d '{"docstatus": 1}'
1 submits, 2 cancels. All the normal validation runs — a document that would fail in the interface fails here, with the same message.
Delete
curl -X DELETE 'https://your-site.com/api/resource/Fabric Test Certificate/FTC-2026-0007' \
-H 'Authorization: token KEY:SECRET'
Calling methods
For data GarmentFlow already assembles, a method call beats reconstructing it from records.
curl -X GET \
'https://your-site.com/api/method/garments_manufacturing.garments_manufacturing.api.production_studio.get_production_overview' \
-H 'Authorization: token KEY:SECRET'
The result always arrives under message:
{ "message": { "batch_board": [...], "open_orders": 14 } }
Some useful ones:
| Method | Returns |
|---|---|
garments_manufacturing.garments_manufacturing.api.production_studio.get_production_overview |
Batch counts by status, open orders, headline KPIs |
garments_manufacturing.garments_manufacturing.api.maintenance_studio.get_maintenance_overview |
Machine health, work-center rollup, upcoming maintenance |
garments_manufacturing.garments_manufacturing.page.floor_monitor.floor_monitor.get_floor_data |
Every work center with its health and fleet KPIs |
garments_manufacturing.api.packing.get_packing_queue |
Orders at the packing operation with counters |
Arguments go as query parameters for GET, or a JSON body for POST:
/api/method/garments_manufacturing.api.packing.get_packing_queue?production_unit=LINE-01
These are internal endpoints, documented here because they are useful. They can change between versions. For an integration that must not break, prefer the resource API on documented DocTypes, or write your own API server script — an endpoint you control, that returns exactly what you need.
Errors
| Status | Meaning | Usual fix |
|---|---|---|
| 200 | Fine | |
| 400 | Bad request | Malformed JSON, or a filter that isn't valid JSON |
| 401 | Not authenticated | Header missing or malformed; check token key:secret |
| 403 | Authenticated, not allowed | The user lacks the role |
| 404 | Not found | Wrong DocType name or record name — check spelling and case |
| 417 | Validation failed | Your data broke a rule; the message says which |
| 500 | Server error | Check the site's Error Log |
Error bodies contain a message and often a server traceback. Log the whole body while developing — the answer is usually in it.
Webhooks — GarmentFlow calling you
Everything above is you calling GarmentFlow. A Webhook is the reverse: GarmentFlow posts to your URL when something happens, so you don't have to poll.
- Awesome Bar →
webhook→ + Add Webhook. - Document Type:
Production Order. - Doc Event:
on_submit(also available:after_insert,on_update,on_cancel,on_trash). - Request URL: your endpoint, e.g.
https://api.yourcompany.com/hooks/gf-order. - Request Method:
POST. - Condition (optional):
doc.total_planned_qty > 1000. - Webhook Headers: add your own auth header so your endpoint can verify the caller.
- Webhook Data: tick the fields to send, or write a JSON template.
- Save.
Two things to build for: your endpoint should respond quickly and do its work afterwards, and it should be idempotent — a retry may deliver the same event twice.
Check Webhook Request Log to see what was sent and what came back.
Building your own app
A phone app for something GarmentFlow's own apps don't cover — a customer order-status app, a supplier drop-off screen, a plant-manager dashboard.
Whatever you build it in, the shape is the same: log in or authenticate, call /api/resource or /api/method, render the JSON.
Option A — FlutterFlow (no code)
FlutterFlow builds iOS and Android apps visually. It is the fastest route if you don't have a mobile developer.
- Create the project. New project, blank app.
- Add the API call. Left sidebar → API Calls → + Add API Call.
- API Call Name:
GetOpenOrders- Method Type:GET- API URL:https://your-site.com/api/resource/Production Order- Headers:Authorization→token KEY:SECRET, andAccept→application/json- Query Parameters:fields=["name","status","end_date","total_planned_qty"]filters=[["status","in",["Released","In Progress"]]]
- Test it. Press Test API Call. You should see the JSON. FlutterFlow shows the response tree — this is where you confirm the path to your data is
$.data. - Create the JSON paths. In the response panel, name the fields you want:
$.data[:].name,$.data[:].status,$.data[:].end_date. - Build the screen. Add a ListView, then a Container inside it. Select the ListView → Generate Dynamic Children → Backend Query → your API call → the
$.datapath. - Bind the text. Drop Text widgets in the container and bind each to a JSON path from the item variable.
- Run it in Test Mode, then publish to the stores.
Filter and search by adding query parameters bound to a text field, and re-running the call.
The key-and-secret approach above is fine for an internal app on managed devices. For anything that leaves your control, use the login flow — call /api/method/login with usr and pwd, keep the session cookie, and drop the static secret.
Option B — Android Studio (Kotlin)
For a native Android app.
Add the dependency in build.gradle:
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
Define the response and the endpoint:
data class Order(
val name: String,
val status: String?,
val end_date: String?,
val total_planned_qty: Double?
)
data class OrderResponse(val data: List<Order>)
interface GarmentFlowApi {
@GET("api/resource/Production Order")
suspend fun getOrders(
@Query("fields") fields: String =
"""["name","status","end_date","total_planned_qty"]""",
@Query("filters") filters: String =
"""[["status","in",["Released","In Progress"]]]""",
@Query("limit_page_length") limit: Int = 50
): OrderResponse
}
Build the client, attaching the auth header to every request:
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "token $API_KEY:$API_SECRET")
.addHeader("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
val api = Retrofit.Builder()
.baseUrl("https://your-site.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GarmentFlowApi::class.java)
Call it from a coroutine:
lifecycleScope.launch {
try {
val orders = api.getOrders().data
adapter.submitList(orders)
} catch (e: Exception) {
Log.e("GarmentFlow", "Fetch failed", e)
}
}
Keep the credentials out of source control and out of the APK — read them from an authenticated login, or from your own backend.
Option C — Xcode (Swift)
For iOS, with no third-party library:
struct Order: Codable {
let name: String
let status: String?
let end_date: String?
let total_planned_qty: Double?
}
struct OrderResponse: Codable {
let data: [Order]
}
func fetchOrders() async throws -> [Order] {
var components = URLComponents(string: "https://your-site.com/api/resource/Production Order")!
components.queryItems = [
URLQueryItem(name: "fields",
value: #"["name","status","end_date","total_planned_qty"]"#),
URLQueryItem(name: "filters",
value: #"[["status","in",["Released","In Progress"]]]"#),
URLQueryItem(name: "limit_page_length", value: "50")
]
var request = URLRequest(url: components.url!)
request.setValue("token \(apiKey):\(apiSecret)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(OrderResponse.self, from: data).data
}
Display it in SwiftUI:
struct OrdersView: View {
@State private var orders: [Order] = []
var body: some View {
List(orders, id: \.name) { order in
VStack(alignment: .leading) {
Text(order.name).bold()
Text(order.status ?? "—").font(.caption)
}
}
.task {
orders = (try? await fetchOrders()) ?? []
}
}
}
Store credentials in the Keychain, never in the source.
Power BI
For statistical analysis, forecasting and board reporting, pull GarmentFlow data straight into Power BI.
Connecting
- Power BI Desktop → Get Data → Web → Advanced.
- URL parts:
https://your-site.com/api/resource/Production Order?fields=["name","status","end_date","total_planned_qty"]&limit_page_length=0 - HTTP request header parameters:
Authorization→token KEY:SECRET - OK, then choose Anonymous for authentication — your credentials are already in the header.
- Power BI opens the Query Editor with a JSON record. Expand
datainto a list, then To Table, then expand the columns.
As an M query
Paste this into the Advanced Editor instead:
let
Source = Json.Document(
Web.Contents(
"https://your-site.com",
[
RelativePath = "api/resource/Production Order",
Query = [
fields = "[""name"",""status"",""end_date"",""total_planned_qty""]",
limit_page_length = "0"
],
Headers = [
Authorization = "token KEY:SECRET",
Accept = "application/json"
]
]
)
),
data = Source[data],
Table = Table.FromList(data, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
Expanded = Table.ExpandRecordColumn(Table, "Column1",
{"name", "status", "end_date", "total_planned_qty"},
{"Order", "Status", "Due Date", "Planned Qty"}),
Typed = Table.TransformColumnTypes(Expanded, {
{"Due Date", type date},
{"Planned Qty", type number}
})
in
Typed
Using RelativePath and Query rather than one concatenated URL is what allows scheduled refresh in the Power BI Service — a hard-coded full URL often won't refresh.
Practical notes
- Pull the tables you need and model in Power BI. Production Order, Production Batch, Operation Ledger Entry and Batch Card as separate queries, related on their key columns, gives you far more than one flattened query.
limit_page_length=0returns everything. Fine for masters and a few thousand orders; for the operation ledger, filter by date and page.- Use a Query Report for heavy aggregation. If the numbers need work, build a Query Report and let the database do it; Power BI then imports a small, ready result.
- Give the Power BI user a read-only role. It never needs write.
- Schedule refresh in the Power BI Service; overnight is usually enough, and it avoids loading the site during production hours.
Excel and Google Sheets
Same data, less setup:
- Excel — Data → From Web, with the same URL and header.
- Google Sheets — an Apps Script
UrlFetchApp.fetch()with the header, writing into a sheet. - Anything — a report exported to CSV, or emailed on a schedule with Auto Email Report.
Securing what you build
- One user per integration, with the least roles that work. You can then see in the change history exactly what each integration did.
- Never embed a secret in anything a user can download. A public app needs either a user login or a backend of yours holding the secret.
- Rotate keys when someone leaves or a laptop is lost — regenerating on the user immediately invalidates the old pair.
- Prefer webhooks to polling. Polling every minute is thousands of pointless requests a day.
- Test against a staging site. An integration that writes records should prove itself somewhere that isn't your live floor.
- Handle failure. The network will drop. Retry with a delay, and make writes safe to repeat.
Troubleshooting
| Symptom | Cause |
|---|---|
| 401 on every call | Header not token key:secret, or the secret was regenerated |
| 403 on some DocTypes | The integration user lacks that role |
Empty data with 200 |
Filters match nothing, or a permission query is restricting the user |
| Child tables missing | You used the list endpoint. Fetch the single document instead |
filters ignored |
Not valid JSON, or not URL-encoded |
| Works in curl, not in code | Your library isn't sending the header, or it re-encoded the URL. Log the request it actually sent |
| Power BI won't refresh in the service | URL built by concatenation — use RelativePath and Query |
What to do next
If you need an endpoint shaped exactly for your integration, write one: API server scripts. If you need it to hold data GarmentFlow doesn't, start at Creating DocTypes.