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/apifyThe 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 theusername/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)
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. WithscrapeReactions 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.
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 —authoris the original author — withrepostedByandrepostedAtsaying who put it on the requested feed, and aheader.textending inreposted this. - a post reshared with commentary (
includeQuotePosts) comes back as the resharer’s own post, with the quoted original nested underrepostand its id echoed asrepostId.
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. Incrementpage_number to walk the
feed:
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:
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 endsFAILED, 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:
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.statusMessageexplains the result ("Confirmed empty: …","Confirmed end of feed: 12 item(s) available, 100 requested.").run.stats.creditsChargedis0— nothing billable was produced.- Reading the dataset directly, the response carries
x-fetchin-run-statusandx-fetchin-run-status-messageheaders, so a consumer that only fetches items still sees the outcome without a second request.
{ "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: truecosts 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 withnumComments: 0ornumLikes: 0is 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
timeoutinput (seconds) bounds a run. If it elapses, the run endsTIMED-OUTand 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
429withRetry-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:
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/hideSendActiondepend on the viewer, not on the post: the same post returnstruefor one caller andfalsefor another. We return what the source tells us; don’t assert on them.document.manifestis not returned. The feed carriesdocument.manifestUrl(which we do return) but not the manifest body — fetch that URL directly if you need it.repost.engagementis 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 itsrepostIdif you need its counts. The run’sstatusMessagesays when this happened.author.urnis 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 —profileIdis 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’sstatusMessagerather than silently returning nothing. contextCountryis 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.sortByis 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 onmaxPosts(orpostedLimit/postedLimitDate, which filter by date server-side) and de-duplicate onid.
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 bypage_number(this matches Apify’s actor). - Reaching a deep
page_numbercosts one source request per skipped page, so page sequentially rather than jumping to a high page number.