Skip to main content

Overview

Fetchin exposes an Apify-compatible endpoint. If you already run B2B data actors through Apify, you can keep your existing code and the official Apify client libraries — you only change the base URL to Fetchin and use your Fetchin API key as the token. Everything runs asynchronously, exactly like Apify: you start an actor run, we queue it, and you poll for the result and read the dataset. Batches are buffered and drained at your plan’s RPS.

Base URL

https://api.fetchin.io/v1/apify
Point any Apify client at that base URL and pass your Fetchin API key as the token. That’s the whole change.
The token here is the same key you send as X-API-Key elsewhere, so rotating it covers this integration too. Create the new key, update the token in your Apify clients, and revoke the old key once it stops being used — both work at once, so nothing breaks in between. Run history and datasets belong to your account, not to a key.

Supported actors

The following Apify actors are supported today. Reference an actor by its raw actor ID or by the username/actor-name form your existing integration already uses — both resolve. We map the output fields to match each actor as closely as possible, so your downstream code keeps working unchanged.
Need another actor? If you rely on an actor that isn’t in this list, reach out via the chat on fetchin.io or email us — we add actors on request.

Usage

JavaScript / Node (apify-client)

Python (apify-client)

HTTP (any language)

Input (posts)

A skipped list is still on every item, as an empty reactions: [] (or comments: []), exactly like the Apify actor. One default differs: when you omit numComments / numLikes, the Apify actor returns 10 of each and we return up to 100. The actor’s other inputs (scrapeUntil, fetchDocumentDetails, rawData) are not supported yet and are ignored.

Input (company posts)

harvestapi/linkedin-company-posts takes a list of page or profile URLs and returns their posts.

One dataset, three kinds of item

This is the part most worth knowing before you switch. With scrapeReactions or scrapeComments on, the dataset is mixed: reactions and comments are their own items, each with a type field, emitted before the post they belong to.
Each post then references them by id:
So filter by type rather than assuming every item is a post:
postNestedReactions / postNestedComments add reactions[] and comments[] arrays on the post item; they do not remove the separate items. Billing: one credit per dataset item, reactions and comments included — which is how the actor itself charges them.

Reshared posts

Two different things, controlled by two different inputs:
  • a post reshared with no commentary (includeReposts) comes back as the original post — author is the original author — with repostedBy and repostedAt saying who put it on the requested feed, and a header.text ending in reposted this.
  • a post reshared with commentary (includeQuotePosts) comes back as the resharer’s own post, with the quoted original nested under repost and its id echoed as repostId.
A page can also reshare someone else’s quote post, in which case the item carries both repostedBy and repost. A collaborated on this header is neither a repost nor a quote post and is never dropped by either input.

Input (comments & reactions)

Both actors accept the published input shape — usernames plus a per-profile limit — and the singular username form:
limit is a ceiling, not a target: a run returns min(limit, available) items per profile. Asking for more than a profile has is safe and returns everything it has — the run’s statusMessage then says Confirmed end of feed. You never need to know a profile’s true count in advance.

Pagination (comments & reactions)

Each run returns one page per profile. Increment page_number to walk the feed:
Reaction items additionally carry a pagination_token. Passing it back with the next page_number jumps straight to that position instead of walking there, so it is the cheaper and faster way to page that actor:
Comment items carry no pagination_token (matching the actor we emulate), so comments page by page_number alone.

Telling “no data” apart from “it didn’t work”

A run that could not collect its data in full ends FAILED, never SUCCEEDED with a short or empty dataset. That covers both shapes: a collection that produced nothing, and one that produced some genuine items but was truncated before the feed was exhausted — the harder case, because the payload looks perfectly valid. A truncated run keeps its items in the dataset (they are real, and yours to use), but its status tells you they are not the whole answer. So run.status is a sufficient check:
When a profile genuinely has no activity of that kind, the run ends SUCCEEDED with a single non-billed sentinel item — { "message": "No comment found for this profile" } / { "message": "No reaction found for this profile" }, matching Apify — and these signals confirm it:
  • run.statusMessage explains the result ("Confirmed empty: …", "Confirmed end of feed: 12 item(s) available, 100 requested.").
  • run.stats.creditsCharged is 0 — nothing billable was produced.
  • Reading the dataset directly, the response carries x-fetchin-run-status and x-fetchin-run-status-message headers, so a consumer that only fetches items still sees the outcome without a second request.
In a multi-profile run, one profile failing does not sink the others: it yields a non-billed { "profile_input": "…", "message": "API request failed", "errorDetails": "…" } item, the rest of the batch completes, and the run only ends FAILED if every profile failed.

Billing, timeouts & retention

  • Billing: 1 dataset item = 1 Fetchin credit (the same per-result accounting as Apify). A batch of 25 profiles = 25 credits. Exception: a post fetched with deepScrape: true costs up to 3 credits per post: 1 for the post plus 1 for each engagement list (its comments, its reactions). A list you turn off with numComments: 0 or numLikes: 0 is not fetched and not billed, so a comments-only post costs 2. A list we could not collect (the fetch failed, or it came back empty although the post shows comments or reactions) is not billed either. A list that is empty because the post genuinely has no comments or reactions is a delivered answer and is billed. A shallow post (deepScrape: false / omitted) always costs 1 credit.
  • Timeout: the timeout input (seconds) bounds a run. If it elapses, the run ends TIMED-OUT and the items already produced remain in the dataset.
  • Retention: run records and datasets are retained for 24 hours. Fetch your results within that window.
  • Rate: your account RPS controls how fast we drain your queued items (not how many runs you can create). Creating runs far faster than your RPS returns 429 with Retry-After; the Apify client backs off automatically.

Differences vs Apify

We validate every actor against real Apify output field-by-field. The reactions, comments and post actors reach zero field errors on matched items; the differences below are values that either cannot be reproduced byte-for-byte (live counts, signed URLs, per-request tokens) or that we approximate where the source data isn’t exposed. Everything here is stable and documented so you can decide whether it affects your integration. If a specific field blocks you, contact us and we’ll prioritise it.

Applies to all actors

  • Live engagement counts (reactions/comments/reposts totals) drift second to second — they reflect the source at fetch time, so two fetches rarely match to the digit. This is identical to Apify.
  • Relative timestamps ("15h", "2w") are wall-clock-relative to the moment of the request. Absolute timestamps (timestamp, postedAtISO, formatted) are exact.
  • Signed media URLs carry an expiring signature (?e=…&v=…&t=…). The host + path are stable; the signature differs every request.
  • Pagination tokens are opaque and positional — use them, don’t compare them.

Batch error handling (profile & post)

The batch actors (profile, post) accept many URLs per run. If one URL can’t be fetched (profile not found, a transient upstream error), the run does not fail: the rest of the batch still completes, the run ends SUCCEEDED, and the failed URL yields a single non-billed item:
You are not charged for a failed URL. Filter results with items.filter((i) => !i.error). A profile that does not exist is final, so don’t retry it; any other error is usually transient and succeeds on a re-run.

supreme_coder/linkedin-profile-scraper (profile)

supreme_coder/linkedin-post (post)

harvestapi/linkedin-company-posts (company posts)

  • socialContent.hideShareAction / hideSendAction depend on the viewer, not on the post: the same post returns true for one caller and false for another. We return what the source tells us; don’t assert on them.
  • document.manifest is not returned. The feed carries document.manifestUrl (which we do return) but not the manifest body — fetch that URL directly if you need it.
  • repost.engagement is omitted, not zeroed, when the quoted post’s own counts aren’t in the feed page. A zero there would read as a fact; fetch the quoted post by its repostId if you need its counts. The run’s statusMessage says when this happened.
  • author.urn is the numeric id for a page and for a member. On the rare item where the feed doesn’t carry a member’s numeric id, we return their opaque id instead rather than invent a number — profileId is always exact.
  • A single-post URL (/feed/update/urn:li:activity:…) cannot be served: it names no author, and the only route to a post’s body is its author’s feed. The /posts/<handle>_… form works. An unservable URL is named in the run’s statusMessage rather than silently returning nothing.
  • contextCountry is accepted and not applied (see the input table).
  • Post order is the feed’s own order, which is relevance-ranked, not chronological, and shifts between calls — the same as the actor itself. Measured on two separate environments: a 10-post window came back 10-05, 10-04, 10-01, 09-30, 09-29, 10-02, 10-02. Don’t rely on two runs returning the same window.
  • query.sortBy is an echo of the actor’s own field, not a guarantee. It reads "date" on every item, including ours, because that is what the actor emits — but the order is the one above. So do not paginate “until I reach the date of my last sync”: a post older than your cutoff can be followed by a newer one, and the walk would stop early while believing it had everything. Page on maxPosts (or postedLimit / postedLimitDate, which filter by date server-side) and de-duplicate on id.

apimaestro/linkedin-profile-comments (comments)

  • Full field parity on matched items. Only the volatile values above (live comment/post reaction counts, relative time) differ.
  • Items do not carry a root pagination_token — this actor pages by page_number (this matches Apify’s actor).
  • Reaching a deep page_number costs one source request per skipped page, so page sequentially rather than jumping to a high page number.

apimaestro/linkedin-profile-reactions (reactions)