FlutterFlow's API Calls are how an app talks to any REST backend β and on the Free plan they're scarce: per FlutterFlow's official plan comparison (September 2026), Free includes 2 API endpoints, and Swagger/OpenAPI import β the "generate all my calls automatically" button β is paid-only. On top of that, the Response & Test tab wants a live URL returning real JSON: you click Test API Call, FlutterFlow shows the response, and the JSON paths you bind widgets to come from what actually came back. No reachable endpoint, no test response, nothing to bind.
So the two slots you get have to carry the whole prototype, and each one needs a real URL behind it from day one. Both problems have the same fix: point the calls at a hosted, stateful mock with uniform conventions β then make one endpoint slot serve every collection. Every curl below was verified against production before publishing.
The public demo project is a seeded e-commerce API. In FlutterFlow: API Calls β + Add β Create API Call:
| Field | Value |
|---|---|
| Call Name | getProducts |
| Method | GET |
| API URL | https://mockbird.mockbird.workers.dev/m/demo/products?limit=20 |
Open Response & Test, click Test API Call β status 200, and the response preview fills with real JSON:
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=3'
# β [ {"id":1, "name":"β¦", "description":"β¦", "price":358.76,
# "category":"β¦", "image":"β¦", "rating":4.6, "inStock":true}, β¦ ]
Now add JSON paths right there in the tab (FlutterFlow suggests them live against the test response): $[:].name β every product name, $[:].price β prices, $[:].image β image URLs. Save, drop a ListView with this call as its backend query, and bind each row's widgets to those paths. Every field in every seeded record is populated with a type-correct value (field types), so the test response you bind against never has the missing-optional-field problem.
FlutterFlow lets you put variables anywhere in the URL with square brackets β their docs' own example turns ?page=0 into ?page=[page], and "changing the base URL with a dynamic URL" is a listed use case. Mockbird's endpoints are uniform β every collection lives at the same shape of URL with the same query params β so one API call definition can serve all of them:
GET https://mockbird.mockbird.workers.dev/m/demo/[resource]?_page=[page]&_limit=20
Create variables resource (String, default products) and page (Integer, default 1), test, save. That's one of your two Free-plan endpoint slots serving:
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?_page=1&_limit=20' # 30 products
curl 'https://mockbird.mockbird.workers.dev/m/demo/orders?_page=1&_limit=20' # orders
curl 'https://mockbird.mockbird.workers.dev/m/demo/customers?_page=1&_limit=20' # customers
curl 'https://mockbird.mockbird.workers.dev/m/demo/reviews?_page=1&_limit=20' # reviews
Each page's backend query passes a different resource value. One caveat, stated honestly: the JSON paths you bind must exist in whichever collection the page loads β bind product paths on product pages, order paths on order pages. Since all four collections return flat typed records under the same conventions, this works better than it has any right to. Your second slot goes to writes (Β§4) β a POST /m/demo/[resource] with a JSON body. Detail fetches don't need a third slot: /[resource]/[id] is just another URL variable.
This isn't an argument against paying FlutterFlow β unlimited endpoints on paid tiers is the real fix β it's how to prototype seriously before that decision.
FlutterFlow's JSON-path examples in the docs work on a wrapped response β {"page":1, "total":3, "data":[β¦]} β because that's what real APIs often return. Mockbird lists are bare arrays by default, but ?mock_envelope reshapes them to match whatever your real API will do:
curl 'https://mockbird.mockbird.workers.dev/m/demo/customers?mock_envelope=data&limit=2'
# β {"data":[ {"id":1,"firstName":"β¦","lastName":"β¦","email":"β¦"}, {β¦} ]}
Now $.data[:].email is the list of emails β the exact path style their docs teach. A custom template goes further and puts pagination in the body, where JSON paths can reach it (FlutterFlow binds body fields, so a body total beats a response header):
# template: {"page":"$page","per_page":"$limit","total":"$total","data":"$data"} (URL-encoded)
curl 'https://mockbird.mockbird.workers.dev/m/demo/customers?_page=1&_limit=3&mock_envelope=%7B%22page%22%3A%22%24page%22%2C%22per_page%22%3A%22%24limit%22%2C%22total%22%3A%22%24total%22%2C%22data%22%3A%22%24data%22%7D'
# β {"page":1, "per_page":3, "total":20, "data":[ β¦3 customersβ¦ ]}
$.total β 20, $.data[:].firstName β names. "Page X of Y" needs no second API call. A project-wide default envelope (set once in settings) keeps the messy param out of your FlutterFlow URLs entirely.
Testing a POST call in the Response & Test tab fires the request for real β against a production API, that's a real record created just to see the response shape. Against the mock it's a record you can see, use in the very next GET, and delete:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products' \
-H 'content-type: application/json' \
-d '{"name":"FlutterFlow guide test","price":12.5}'
# β {"id":31, "name":"FlutterFlow guide test", "price":12.5}
curl 'https://mockbird.mockbird.workers.dev/m/demo/products/31' # really persisted
curl -X DELETE 'https://mockbird.mockbird.workers.dev/m/demo/products/31'
Body variables use the same square-bracket substitution β {"name":"[name]","price":[price]} with variables defined in the Variables tab β so the create-form β list-refreshes β delete loop of your app runs end-to-end while the real backend is still a Figma comment. The demo dataset resets every 24 hours, so experiments clean themselves up.
An API Call action in the Action Flow Editor branches on success or failure β but you can't build the failure branch (snackbar, retry button, error page) against an API that never fails. One URL param:
https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=500
Run the page: the failure branch executes, your snackbar shows, and you can screenshot the error state for the design review. Remove the param and the same call is green again. For "fails twice then recovers" β what pull-to-refresh and retry buttons should survive β use a deterministic sequence:
# 1st request β 503, 2nd β 503, 3rd and later β real 200
'https://mockbird.mockbird.workers.dev/m/demo/products?mock_seq=503,503,200&mock_seq_key=myapp'
# run four times: 503 503 200 200
curl -s -o /dev/null -w '%{time_total}\n' \
'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=3000'
# 3.77s
Backend queries show a loading indicator while the call is in flight β with ?mock_delay=3000 (up to 5,000 ms) that state lasts long enough to design: shimmer, skeleton rows, a disabled submit button. Test mode on fast Wi-Fi never shows you what users on mobile data see; a mock that's slow on purpose does.
FlutterFlow's docs note that inside the builder, Run mode, and Test mode, API calls are routed through a FlutterFlow proxy to avoid CORS issues. Mockbird sends permissive CORS headers on every endpoint, so calls work with the proxy, without it, and browser-direct from a deployed web build β one less variable when a call mysteriously fails only in one mode.
FlutterFlow's docs teach exactly this header for authenticated APIs: Authorization: Bearer [auth_token], with the token saved to persisted app state after login. Mockbird's mock auth gives you the whole flow to build against β a login call that returns a real signed JWT for any email/password:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/auth/login' \
-H 'content-type: application/json' \
-d '{"email":"pat@example.com","password":"anything"}'
# β {"token":"eyJhbGciOiJIUzI1NiIsβ¦", "tokenType":"Bearer", "expiresIn":3600, "user":{β¦}}
Make it an API call, bind $.token, store it in app state, pass it as [auth_token] β the exact wiring their docs describe, minus needing an auth backend. On your own project, protected mode makes every endpoint genuinely require the token, so the missing-token failure path is testable too. (If your app's auth is Firebase or Supabase, use FlutterFlow's built-in integrations instead β this is for apps authenticating against a custom REST API.)
When a POST "should work" but doesn't, the question is what left the app after every [variable] was substituted. Point the call at a bin:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/hooks/ff-init' \
-H 'content-type: application/json' -d '{"app":"ff-mvp","step":1}'
# β {"ok":true,"caught":"POST /hooks/ff-init","body":{β¦},"receivedAt":"β¦"}
The demo's catch-all route echoes what it caught, and every hit β method, path, headers, body β lands in the public demo request inspector. Great for catching a number that arrived as a string, or a header your shared config didn't actually add. On your own project, one ANY /* custom route is a private bin.
Because the mock speaks the same shape your real API will, cutover is editing the base URL (or the default of a base-URL variable β their docs' dynamic-base-URL pattern makes this a one-field change). Two tools keep the shapes honest:
/m/<project>/openapi.json β import it and FlutterFlow generates all the API call definitions for you. (On Free, the Β§2 variable trick replaces the import button.)One curl (or one click), no signup:
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H "content-type: application/json" \
-d '{"preset":"ecommerce"}'
# β {"id":"abc123xyz9", "adminKey":"β¦", ...}
Presets: blog / ecommerce / saas β or define resources matching your app's data. Every simulation param above works on your project's URLs, plus snapshots for demo-day datasets you can restore before every stakeholder walkthrough. Projects have a 10,000 requests/day cap β app building barely dents it.
Fair's fair: if your app's data should live in Firebase or Supabase, FlutterFlow's first-class integrations for both are deeper than any REST wiring β schema-aware queries, auth, realtime β and need none of this. API Calls (and this guide) are for the other case: your app talks to a custom or third-party REST backend that isn't built, isn't safe to test against, or won't fail on demand. And the 2-endpoint Free limit is FlutterFlow's pricing working as intended β if the app outgrows the Β§2 trick, the upgrade buys unlimited endpoints and the Swagger import that pairs beautifully with a mock's exported spec.
Related: the Adalo version, the Flutter (code) version of this guide, the Bubble version, the WeWeb version, the Thunkable version, the Retool version, the Appsmith version, testing loading and error states, mock JWT auth, and mocking third-party APIs generally.
Verification: all demo curls on this page (limit=3 typed list, single record, orders/customers/reviews under identical conventions, mock_envelope=data wrap, custom envelope returning {"page":1,"per_page":3,"total":20,"data":[β¦]}, mock_status 500, 3.77s measured delay, seq 503β503β200β200, POSTβGET-backβDELETE with id 31, auth login token shape, openapi.json 200, bin echo, create-project preset) were run against production on 21 Sep 2026 before publishing. FlutterFlow facts β Free plan's 2 API endpoints and paid-only Swagger/OpenAPI import (official plan comparison), square-bracket URL/header/body variables, the dynamic-base-URL pattern, the Response & Test flow, JSON-path binding, the test/run-mode CORS proxy, and the Bearer [auth_token] header pattern β are from FlutterFlow's official docs as of September 2026. We don't run your FlutterFlow app; if a step here doesn't work, that's a bug: tell us.