Bootcamp
API calls as actions vs data (and the planning checklist)
Photo by Ben McCloskey on Unsplash
“Oh, it’s just an API integration. Surely that’s a few hours? Isn’t there a plugin?” Anyone who builds software for other people learns to smile at that sentence. A robust integration that connects to five, ten or fifteen endpoints, authenticates properly, decides intelligently where data lives and never fails silently is routinely two or three days of work, sometimes a week. The gap between “it’s just an API” and reality is design, and that’s what this article is about.
Three design decisions come up in every integration a Bubble builder ships: whether each call is an action or a data source, whether the app connects to one external account or to many, and the dozen questions you should answer on paper before touching the API Connector at all.
An API call in Bubble is either an action or a data source. An action is a workflow step that makes something happen: create an invoice, update a customer, shorten a link. A data call behaves like a live table you can point a repeating group at. Choosing correctly, per call, shapes everything downstream.
Actions: calls that make something happen
When a call’s Use as setting is Action and the call has been successfully initialised, with the response’s JSON fields mapped into types you can use, it appears in the workflow editor’s actions list like any other step. That’s the whole mechanism, and both conditions matter. An uninitialised call won’t appear. A call set to Data won’t appear in the actions list either (and an Action never appears among data sources). This asymmetry has caused every Bubble developer real distress at some point: you sit there wondering why your call is missing, ready to take to the forums, when it’s almost always one of those two causes.
One durable warning about actions: initialisation is what creates the result fields your workflows reference. If you later re-initialise the call, or restructure it, say by moving values between the body and the parameters, you can silently break every workflow step that referenced the old fields. Get the call right, then touch it as little as possible.
Data: calls that behave like a table
Set Use as to Data and the call shows up somewhere else entirely: in dynamic expressions, as Get data from an external API. Instead of Do a search for, which queries your database, you’re pulling records straight off someone else’s server.
Here’s a real walkthrough using Bitly, the link shortener, and it doubles as an honest picture of what playing with APIs is like, because the endpoint we wanted needed two others first.
- Find the endpoint. The goal: list all our shortened links. Bitly’s reference shows that bit links are retrieved by group, the group endpoint needs a group GUID, and groups are retrieved by organisation. A chain of three calls, just to get a list. Welcome to APIs.
- List organisations. Create a GET call to the list-organisations endpoint (private-key auth, as with any Bitly call). One lesson from initialising it: our first attempt returned a 404 because the URL said “organization” where it needed “organizations”. One character can make a world of difference. The response contains the organisation’s GUID; copy it.
- Retrieve groups. A second GET call to the groups endpoint, with a
organization_guidparameter (name your parameters accurately; it pays off later). Back comes one group and its GUID. - List bit links. The main call: GET, with the group GUID passed as part of the URL path, not as a parameter, which the docs make clear. The reference lists a pile of optional search parameters you can ignore. Set Use as to Data, initialise, and there they are: every shortened link with its long URL.
- Prune the response. At initialisation you can mark fields you don’t need as ignored. Do it; it tidies the list of options you’ll scroll through later.
Now the interesting part. Initialising a data call creates a new data type in your app, defined not by your database but by the API’s response structure. Drop a repeating group on the page and set its data source to Get data from an external API, choosing the bit links call. The type won’t match at first: the response nests a list called links inside the outer object, so the repeating group’s type is the inner link type, and the source expression ends with 's links. Bubble’s names for these can be a mouthful (“List bit links links”); you can rename the call to follow the documentation’s own naming so the expressions read sensibly.
Inside the cell, add texts bound to Current cell's ... long_url and ... link, give the row a container layout with a spacing gap, and preview. Every row is being pulled straight from Bitly’s server at page load; none of it touches your database. Inspect the page with the debugger and you can see it: the repeating group’s data source is the API call, link by link.
Data calls also work in workflow expressions, so you can pull external data and save it. You could even add a field on one of your data types whose type is the API-defined type and store the list itself, which opens some interesting architecture options.
The rule of thumb the whole distinction collapses into: use an action when you want to trigger something (create an invoice, update a customer), use data when you want to show something, typically a list.
One app, many accounts: the one-to-many pattern
Everything so far assumes one Bubble app talking to one external account: your app, your Bitly key, your email marketing platform. Every contact created in your CRM gets added to your Mailchimp list. One-to-one.
But what if each user of your app needs to connect their own account? Their own Bitly, their own email tool, their own Shopify. The shared-header approach breaks immediately, because a shared header is one key for everyone.
The pattern:
- Move the authorization header out of the shared headers (and out of Bubble’s built-in auth options; set the API back to self-handled). Keep genuinely shared headers, like the JSON content type, where they are.
- Add the
Authorizationheader on the individual call, and untick private. Private means one fixed value; exposing it to the app means every workflow can supply its own value, which is exactly what you want here. - Store each customer’s token in your database, and store it on the Account, not the User. That way the account owner saves the token once and every team member on that account inherits the connection, rather than every user acting as their own island. The association runs through the account relationship you already have in your data model.
- Protect it with privacy rules. The rule on Account: when the current user’s account is this account, they can see the token; otherwise nobody can. You never expose one customer’s token to another.
- Build the header value dynamically in the workflow: arbitrary text
Bearer, a space, thenCurrent User's Account's Bitly access token.
Testing this teaches one more lesson for free. Run it logged out and the call comes back Forbidden: the current user has no account, so the token expression is empty, and the debugger’s step-by-step view shows exactly that, an empty account and an empty token. Use “run as” to test as a real user and it works. The failure isn’t a bug; it’s your privacy rules doing their job, and it’s a preview of the errors your real users will generate.
Calling your own app’s API
Here’s a pattern that surprises people: the API Connector pointed at your own Bubble app. It’s a genuinely useful way to test backend workflows and to understand your app as a service.
- In Settings, enable the Data API (ticking the data types to expose) and the Workflow API, then generate an API token; name it something like “internal use”.
- In the API Connector, create a new API called
internal, authentication private key in header, and paste the token. - Add a GET call to your Data API’s contacts endpoint and initialise. Back come your actual database records, every contact, all thirteen entries in our CRM’s case, with all their fields. The Data API supports more than GET: you can POST updates and DELETE records too.
- For the Workflow API, create a backend workflow (exposed as a public endpoint) containing a Return data from API action that returns
Do a search for Contacts. Call it through the connector; the URL differs from the Data API’s only in that it targets workflow endpoints. Same result, different door.
The difference between the doors matters. The Data API is essentially limited to get, create, update and delete on exposed types. A workflow endpoint can do almost anything: accept parameters, run calculations, update multiple data types, then return exactly what you choose. When you need flexibility, the Workflow API is the one.
The planning checklist
These are the questions we document in our agency before, or while, connecting to any new API. Twenty per cent of integrations get built with twenty per cent of these answered, and then everyone wonders why it isn’t achieving what they wanted.
- Is this even the right service? Look at the API before you commit to the product: the docs tell you what it can actually do for you. A service might do what you want in its UI but not expose it via API, which can make it useless to you.
- How will I handle test and live environments? Financial services like Stripe give you a sandbox by default; most APIs give you one account and one key. Options: a second account, a free developer sandbox if offered, or, worst case, developing against live and deleting the test data. Any of these can work; having no plan cannot.
- How do I authenticate? One app to one service, or each user connecting their own account (the one-to-many pattern above)? A static key, or OAuth with rolling tokens? Where will keys and tokens be stored?
- Which directions does data flow? Think in CRUD terms. If you’re both pulling and pushing, how do you avoid losing data, pushing empty fields over real values, or overwriting the other side’s more recent edit with your stale copy? One way, both ways, or both depending on the record?
- Where does the data live, and when do you sync? The temptation is always to import everything and keep it in sync. Equally valid: leave data on the external system and pull it on demand. Systems can also be decoupled in time; daily sync, on-demand sync or no sync at all are legitimate designs depending on the use case.
- Does the API offer webhooks? Webhooks push you the information when the event happens; polling, the bad old days, means hammering the server on a schedule and discarding most of what comes back, at cost to both sides. Prefer webhooks whenever they exist.
- Which endpoints do I need, and are they idempotent? If you send the same request twice, or receive the same webhook twice, do you get the same end state, or have you just double-created something?
- How will I handle errors? Let the default popup fire, branch the logic, notify someone when something breaks. APIs change, go down, and tokens expire; that’s normal maintenance reality. What you must not build is a system that fails silently, leaving a user stuck, frustrated, and convinced your app is buggy when the truth is nobody wrote the error path.
Write your answers down per integration. It’s the difference between “just an API integration” and one that’s still working in a year.
This skill in the AI era
This checklist is the most portable artefact in the whole module: sync direction, token storage, idempotency and error paths are exactly the questions we work through when auditing an app’s integrations during a Bubble rescue or migration, and the answers are what we hand to Claude Code as the spec when rebuilding them. AI writes integration code fluently, but it inherits your design decisions, good or absent, and a missing answer to “what happens when the token expires” fails the same way in Python as it did in Bubble. Thinking in actions versus data, one account versus many, is the kind of judgement that moved with us from Bubble to AI-assisted development.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
Updating data in Bubble: auto-binding vs workflows
When to update Bubble records with Make changes to a thing workflows, when auto-binding is the better fit, the privacy rule it needs, and safe deleting.