API and integrations
LinkedFlow talks to the rest of your stack in three ways: a REST API with a token, a webhook that fires workflows from outside, and nodes that call your own services from inside a journey. This page is the exact contract, with examples ready for n8n, Make or a plain curl.
The API the app itself uses is the public API: same endpoints, same authentication. For an integration, do not use the browser's token: create an API token in Settings → API tokens, with the expiry you choose and bound to this workspace.
The three paths
| Path | Who calls whom | What it is for |
|---|---|---|
| REST API | Your tool → LinkedFlow | Create prospects and lists, launch workflows, read executions, resolve approvals, pull the funnel. |
| Inbound webhook | Your tool → one specific workflow | Start a journey for a prospect that already exists in the CRM, with the workflow token as the credential. |
| Outbound nodes | LinkedFlow → your tool | Notify your CRM or n8n mid-journey (Outbound webhook) or ask it for a value and carry on with the answer (HTTP request). |
Authentication
The whole API lives under https://app.linkedflow.pro/api/v1 and authenticates with a token in the Authorization header. There are two ways to get one, and only the first is meant for integrations.
API token (the one you want for n8n)
In the app: Settings → API tokens → New token. Give it a name ("n8n · daily signals"), choose an expiry — 30 days, 90 days, 1 year or never — and copy it: it is shown only once.
| Property | What it means |
|---|---|
| Bound to one workspace | The one it was created in, and it cannot move: even if you switch workspace in the app or someone sends the X-Workspace header, that token keeps writing where it was configured. |
| Inherits your role | It grants nothing extra. Drop from MANAGER to VIEWER and its write calls start returning 403. |
| Survives your password | Changing your password closes other browser sessions but does not revoke API tokens: rotating a password never takes your integrations down. |
| Revoked instantly | From the same screen. It stops working on the next call, and that screen also shows when it was last used. |
Tip: create a separate user for the machine ("automation@yourcompany.com") with the smallest role it needs and issue the token from that account. The workspace activity then tells apart what a human did from what n8n did, and you can cut its access without touching yours.
Session token (for a throwaway script)
The app's own login returns a token too, but it expires after 30 days and follows your user's active workspace. Fine for a quick curl; not for automation that has to last months:
curl -X POST https://app.linkedflow.pro/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"email":"you@email.com","password":"your-password"}'
{
"user": { "id": "…", "name": "Valen", "current_workspace": { "id": "…", "role": "OWNER" } },
"token": "198|eMdBPK8g…"
}
From then on, every call carries two headers:
Authorization: Bearer 198|eMdBPK8g…
Accept: application/json
The token is your password. Store it as a credential in n8n (Header Auth: name Authorization, value Bearer <token>), never in a URL or in a node body. If an API token leaks, revoke it in Settings → API tokens: it is immediate and affects no other token.
Workspace and roles
You never send the workspace: the token always works on the user's current workspace. If you belong to several, POST /auth/switch-workspace/{workspace} switches. Every endpoint demands the same role as the equivalent screen in the app (VIEWER < MEMBER < MANAGER < ADMIN < OWNER); if you fall short, you get a 403. Roles are explained in Settings.
Tip for integrations: create a separate user for the machine ("automation@yourcompany.com") with the smallest role it needs. You can cut its access without touching yours, and the workspace activity tells apart what a human did from what n8n did.
Conventions
- No
Accept: application/json, no JSON: the app would answer with a redirect or HTML. It is the number one beginner mistake. - Errors:
401missing or expired token,403insufficient role or inactive subscription,404outside your workspace,422validation ({"message":"…","errors":{"field":["…"]}}),429too many calls. - Collections: prospects come paginated (
data,current_page,last_page,total); other resources return the plain collection or wrap it initems. Always look at the first response before mapping fields. - Language: error messages come in your user's language. Force it with
Accept-Language: en. - Pace: there is no global rate limit, but expensive endpoints have their own (the workflow webhook, 30 calls per minute; the AI composer, 20).
Main endpoints
The full list is the app's own; this is what actually gets used from outside.
Prospects and lists
| Endpoint | Role | What it does |
|---|---|---|
GET /prospects | VIEWER | Paginated list. Filters: search, status, list_id, tag. |
POST /prospects | MEMBER | Creates a prospect. Takes list_id to drop it into a list on the way in. |
GET /prospects/{id} | VIEWER | Full record. |
PUT /prospects/{id} | MEMBER | Edits fields (not the status: the engine moves that). |
POST /prospects/bulk-tags | MEMBER | Tags in bulk. |
GET /prospects/{id}/conversation | VIEWER | The real LinkedIn thread. |
POST /prospects/{id}/conversation/messages | MANAGER | Sends a real DM (quotas and working hours still apply). |
GET · POST /lists | VIEWER · MEMBER | List and create lists. |
POST /lists/{id}/prospects | MEMBER | Adds existing prospects to a list. |
POST /suppression | MANAGER | Sends someone to the suppression list. |
Workflows and executions
| Endpoint | Role | What it does |
|---|---|---|
GET /workflows | VIEWER | List with status, current version and latest execution. |
POST /workflows/{id}/run | MANAGER | Runs over list_id or prospect_ids[]. Accepts account_id and allow_repeat. |
POST /workflows/{id}/activate · /pause | MANAGER | Turns the triggers on or off. |
POST /workflows/{id}/validate · /dry-run | MEMBER | Validates and simulates without touching LinkedIn. |
GET /executions · /executions/{id} | VIEWER | Executions and their detail with per-node KPIs. |
GET /prospect-executions/{id} | VIEWER | One person's journey, node by node. |
POST /executions/{id}/cancel | MANAGER | Stops whatever is still pending. |
GET /approvals · POST /approvals/{id}/approve | VIEWER · MANAGER | The approvals inbox from outside — from Slack, for instance. |
GET /dashboard/funnel · /stats · /activity | VIEWER | The dashboard numbers, for your own reports. |
GET /errors/unmanaged · /paused | VIEWER | Whatever is waiting on a human. Perfect for a daily alert. |
The recipe that solves 80 % of it: create and fire
A new prospect lands in the CRM and, if you drop it into a list, the workflow listening to that list starts on its own. Two calls, or even one:
curl -X POST https://app.linkedflow.pro/api/v1/prospects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"linkedin_url": "https://www.linkedin.com/in/someone",
"first_name": "Ana",
"company": "Northern Consulting",
"tags": ["n8n", "signal:hiring"],
"source": "n8n",
"list_id": "01a05d4e-23b2-725c-9aa4-e1ed9e8fd6da"
}'
Where does the list_id come from? From the app: Prospects → click a list to select it → Copy ID. Over the API, GET /lists returns each list's id. It is a UUID (01a05d4e-23b2-725c-…); send anything else and you get a 422 saying list_id is not valid.
Three things worth knowing about this call:
- Either
linkedin_urlorpublic_identifieris enough. If the URL is not a profile, you get a422. - A duplicate answers
409and that is not a failure: it returns the prospect that already existed and adds it to the list anyway. In n8n, configure the node not to abort on 409 — it is the normal path when you reprocess a source. - Landing in the list fires the "When added to a list" trigger of any active workflow listening to it, whether the prospect is brand new or already existed and simply was not in it. That is the hook: n8n needs to know nothing about workflows, only how to fill a list.
A list_id that is a well-formed UUID but does not exist (or belongs to another workspace) is not an error: the prospect is created and joins no list. That is tenant isolation — the response must not reveal which lists someone else has — so check the lists field in the response if you want to be sure it landed where you meant.
If you would rather fire explicitly, use POST /workflows/{id}/run with {"list_id":"…"} or {"prospect_ids":["…"]}. It needs the MANAGER role and a saved workflow version.
Inbound webhook: firing a workflow from outside
A workflow that starts with the Webhook node exposes a URL with its own token (visible in the builder, MANAGER or above):
POST https://app.linkedflow.pro/api/v1/webhooks/workflows/{token}
Content-Type: application/json
{ "linkedin_url": "https://www.linkedin.com/in/someone" }
Valid identifiers, at least one: public_identifier, linkedin_url or email. Anything else you send is available inside the journey as {{trigger.*}}, so you can pass the reason for the signal and use it in the message copy.
| Response | Meaning |
|---|---|
202 {"status":"accepted"} | Received. It is always this one, whether the prospect exists or not and whether the journey starts or not. |
404 | Unknown token, workflow not active, or its first node is not a Webhook. |
422 | You sent no identifier at all. |
This endpoint does not create prospects. It resolves one already in your CRM; if it finds nobody it still answers 202 and nothing happens. That is deliberate: the response must not become a way to find out who is and is not in your database. If your integration brings new people, create them first with POST /prospects.
A 202 does not guarantee the journey starts either: a suppressed prospect, one marked do-not-contact or one who already replied stays out because of the usual protections. What actually happened is visible in Executions.
Outbound: when LinkedFlow calls your tool
Outbound webhook
The Outbound webhook node POSTs to whatever URL you set. With no custom body, it sends this:
{
"prospect": {
"id": "01a0…",
"full_name": "Ana García",
"linkedin_url": "https://www.linkedin.com/in/…",
"company": "Northern Consulting",
"status": "REPLIED"
},
"execution_id": "01a0…",
"node_id": "webhook_1"
}
The POST is not signed, and the node has only two fields: method and URL. No headers, no custom body — so you cannot send a signature or a secret in a header. Anything you want to travel along goes in the URL, which does accept variables:
https://your-n8n/webhook/replies?s=A-LONG-SECRET&cat={{previous.classification}}
In n8n that arrives as {{ $json.query.s }} and {{ $json.query.cat }} — not in headers or body. Check the secret with an If node right after the webhook and drop whatever lacks it: without that check, anyone who finds the URL can inject fake data.
Operational details: 15-second timeout; a 5xx counts as transient and is retried up to three times with growing waits; a 4xx is marked as a configuration failure and waits for your decision in the errors inbox. Answer fast and with 200: if your automation is slow, accept first and process afterwards.
HTTP request: ask for a value and carry on
The HTTP request node is the same thing, meant for using the answer. Whatever you return is available in the next node:
{{previous.status}} → 200
{{previous.response.tier}} → "A" (your JSON, under "response")
{{previous.response.angle}} → "hiring SDRs"
With that, an If node branches on your own scoring: {{previous.response.tier}} == "A". Always return flat JSON and answer in under 15 seconds; if your service can be slow, return a default value rather than making the journey wait.
Five n8n flows that work
1 · External signal → outreach (capture)
Schedule at 07:30 → HTTP Request to your signal source (posts, job changes, job ads) → Code normalising to linkedin_url → Remove Duplicates → HTTP Request to POST /prospects with the list_id of "Today's signals" and the signal in tags.
In LinkedFlow: an active workflow triggered by When added to a list → Read profile → AI score → If {{previous.score}} >= 70 → React to their latest post → Wait 1 day → Send invitation with a note. Configure the n8n node to treat 409 as success.
2 · Classified reply → CRM and calendar (handle)
In LinkedFlow: Message received trigger → Classify reply node → every output into the same Outbound webhook, with the category in the URL: ?cat={{previous.classification}}.
In n8n: Webhook → an If comparing {{ $json.query.s }} with your secret → Switch on {{ $json.query.cat }}: INTERESTED creates the deal in your CRM, pings Slack and sends the booking link; QUESTION drafts a reply with AI and drops it in Slack for approval; NOT_NOW writes the follow-up date in a sheet and a cron re-injects it after 90 days; DO_NOT_CONTACT calls POST /suppression and closes.
3 · "Comment GUIDE and I'll send it"
In LinkedFlow: Comment on a post trigger with a keyword → React to the comment → Reply to the comment → Send message with the link → Outbound webhook.
In n8n: add them to your email tool, write a row in a sheet with commented → delivered → opened, and after three days, if there was no reply, POST to the inbound webhook of a follow-up workflow. Remember comments are polled every 5 minutes and that replying to a comment does not spend message quota.
4 · Your own enrichment and scoring mid-journey
Here LinkedFlow calls n8n. In the journey: Read profile → HTTP request to your n8n webhook (respond with the last node) → If {{previous.response.tier}} == "A" → invitation with a tailored note; otherwise into a nurturing list.
In n8n: Webhook → scrape the company site → an AI model returning strict JSON (tier, angle, pain) → Respond to Webhook. Add a fallback (tier: "B") so a failure on your side never jams the journey.
5 · The Monday report (watch)
Schedule Monday 07:00 → GET /dashboard/funnel and GET /executions → GET /errors/unmanaged → an AI model prompted for "three decisions, not three charts" → email. No new dashboards: the numbers come from the same API that paints the app's own.
Download them ready-made
Four n8n workflows ready to import from file. Before switching them on: create a Header Auth credential named Authorization with the value Bearer <your-token>, and replace the sample list id with your own.
- 1 · External signal → prospect in a list — the one that solves 80 %.
- 2 · Classified reply → CRM and calendar — includes the secret-header check.
- 3 · Enrichment and scoring — the one that answers the HTTP request node.
- 4 · The Monday report — funnel, executions and errors over the API.
Good practice
- Deduplicate before calling, by
linkedin_urland day. Two identical POSTs will not duplicate the prospect, but they can push it through the flow twice if you remove it from the list and add it again. - Do not fight the quotas. However fast you push, actions leave at the account's pace (25 visits, 20 invitations and 30 messages a day by default). Queueing 500 prospects speeds up nothing; it only fills the queue.
- Log the
execution_idthat arrives in outbound webhooks: it is the key to finding that journey again in the app. - One user per integration, with the smallest role, and rotate its token when it changes hands.
Missing an endpoint? Write to us from the support chat in the app: what gets documented and opened up first is decided by the integrations people are actually building.