LinkedFlow Docs Open the app

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

PathWho calls whomWhat it is for
REST APIYour tool → LinkedFlowCreate prospects and lists, launch workflows, read executions, resolve approvals, pull the funnel.
Inbound webhookYour tool → one specific workflowStart a journey for a prospect that already exists in the CRM, with the workflow token as the credential.
Outbound nodesLinkedFlow → your toolNotify 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.

PropertyWhat it means
Bound to one workspaceThe 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 roleIt grants nothing extra. Drop from MANAGER to VIEWER and its write calls start returning 403.
Survives your passwordChanging your password closes other browser sessions but does not revoke API tokens: rotating a password never takes your integrations down.
Revoked instantlyFrom 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: 401 missing or expired token, 403 insufficient role or inactive subscription, 404 outside your workspace, 422 validation ({"message":"…","errors":{"field":["…"]}}), 429 too many calls.
  • Collections: prospects come paginated (data, current_page, last_page, total); other resources return the plain collection or wrap it in items. 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

EndpointRoleWhat it does
GET /prospectsVIEWERPaginated list. Filters: search, status, list_id, tag.
POST /prospectsMEMBERCreates a prospect. Takes list_id to drop it into a list on the way in.
GET /prospects/{id}VIEWERFull record.
PUT /prospects/{id}MEMBEREdits fields (not the status: the engine moves that).
POST /prospects/bulk-tagsMEMBERTags in bulk.
GET /prospects/{id}/conversationVIEWERThe real LinkedIn thread.
POST /prospects/{id}/conversation/messagesMANAGERSends a real DM (quotas and working hours still apply).
GET · POST /listsVIEWER · MEMBERList and create lists.
POST /lists/{id}/prospectsMEMBERAdds existing prospects to a list.
POST /suppressionMANAGERSends someone to the suppression list.

Workflows and executions

EndpointRoleWhat it does
GET /workflowsVIEWERList with status, current version and latest execution.
POST /workflows/{id}/runMANAGERRuns over list_id or prospect_ids[]. Accepts account_id and allow_repeat.
POST /workflows/{id}/activate · /pauseMANAGERTurns the triggers on or off.
POST /workflows/{id}/validate · /dry-runMEMBERValidates and simulates without touching LinkedIn.
GET /executions · /executions/{id}VIEWERExecutions and their detail with per-node KPIs.
GET /prospect-executions/{id}VIEWEROne person's journey, node by node.
POST /executions/{id}/cancelMANAGERStops whatever is still pending.
GET /approvals · POST /approvals/{id}/approveVIEWER · MANAGERThe approvals inbox from outside — from Slack, for instance.
GET /dashboard/funnel · /stats · /activityVIEWERThe dashboard numbers, for your own reports.
GET /errors/unmanaged · /pausedVIEWERWhatever 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_url or public_identifier is enough. If the URL is not a profile, you get a 422.
  • A duplicate answers 409 and 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.

ResponseMeaning
202 {"status":"accepted"}Received. It is always this one, whether the prospect exists or not and whether the journey starts or not.
404Unknown token, workflow not active, or its first node is not a Webhook.
422You 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.

Good practice

  • Deduplicate before calling, by linkedin_url and 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_id that 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.