API Docs/Reference/Fetch Saved Posts
API Reference

Fetch Saved Posts

Retrieve all the bookmarks that you've created. You can use the query parameters to filter and paginate results.

GEThttps://api.linkedmash.com/v1/bookmarks

GET /v1/bookmarks is the endpoint most integrations start with. It returns the LinkedIn posts (and saved comments) the authenticated account has collected in LinkedMash, newest first, as a cursor-paginated list. Each item carries the post text, permalink, attachments, the author's name, bio and profile image, your labels, read and archive state, and both posted_at and imported_at timestamps — enough to mirror a library into another system without a second call.

The same filters the LinkedMash web app uses are exposed here, and they behave identically: read and unread posts are both returned unless you ask otherwise, and archived posts are hidden by default. Keyword search (q) matches post content; vector_search_term runs semantic search over the same library and needs a vector store that has been initialised at least once.

Example request

Authenticate with a bearer token in the Authorization header. Create a key on the API page in your LinkedMash settings and export it as LINKEDMASH_API_KEY before running this.

curl
curl "https://api.linkedmash.com/v1/bookmarks?limit=20&author=Dickie%20Bush&posted_from=2026-01-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer $LINKEDMASH_API_KEY"

Header parameters

AuthorizationstringRequired

Your secret API key. Should be provided as a Bearer token.

Query parameters

Combine these parameters to narrow your results.

limitintegerOptional

The maximum number of bookmarks to return. Default is 20, maximum is 100.

cursorstringOptional

Cursor for pagination. Use the next_cursor value from a previous response.

is_unread_onlybooleanOptional

Return only unread posts. Defaults to false — read and unread are both returned unless you set this to true.

hide_archivedbooleanOptional

Filter by archived status. Default is true (hides archived bookmarks).

media_typestringOptional

Filter by media type. Must be one of image, video or pdf; any other value returns 400. Paid plans.

commentstringOptional

Filter by comment: 'only' returns just saved comments, 'exclude' returns only top-level posts (excludes comments). Paid plans.

authorstringOptional

Filter by author name.

tagstringOptional

Filter by tag.

posted_fromdate-timeOptional

Filter bookmarks posted after this date/time (ISO 8601 format).

posted_todate-timeOptional

Filter bookmarks posted before this date/time (ISO 8601 format).

bookmarked_fromdate-timeOptional

Filter bookmarks created after this date/time (ISO 8601 format).

bookmarked_todate-timeOptional

Filter bookmarks created before this date/time (ISO 8601 format).

sort_bystringOptional

Field to sort by (e.g., 'created_at', 'title').

sort_cursorstringOptional

Value of the sort field from the last item of the previous page, used for stable sorting with pagination.

qstringOptional

Search query to filter bookmarks by title or content.

vector_search_termstringOptional

Vector search term for semantic search.

Response

{
  "status": true,
  "data": [
    {
      "post_id": "7336731872414035968",
      "post_details": {
        "text": "Every single night, I run through a dead-simple 2-minute journal prompt.\n\nIt only has 5 questions.\n\nHere's how it works:",
        "link": "https://www.linkedin.com/feed/update/urn:li:activity:7336731872414035968",
        "posted_at": "2025-06-06T12:33:08.27+00:00",
        "attachments": [
          {
            "type": "document",
            "title": "My 2-Minute Evening Journal Prompt (5 Questions)",
            "url": "https://media.licdn.com/dms/document/media/v2/D4E1FAQF_Jxg36sxpIg/feedshare-document-pdf-analyzed/B4EZdFOIHNGcAc-/0/1749213108945?e=1750291200&v=beta&t=HmRZA2gVOuUaGdOqfKz5WDWcnJ_fKWYSv-hXEhg4WvA",
            "thumbnail": [
              {
                "url": "https://media.licdn.com/dms/document/pl/v2/D4E1FAQF_Jxg36sxpIg/feedshare-document-images_1920/B4EZdFOIHNGcAY-/0/1749213108945?e=1750291200&v=beta&t=UTLn8IylWinjDWp31RgkPOa-ImYgdpePUEclQuy-xjw",
                "width": 1545,
                "height": 1931
              }
            ]
          }
        ]
      },
      "author_id": "urn:li:member:414777096",
      "author_details": {
        "bio": "I talk about digital writing & personal progress",
        "name": "Dickie Bush 🚢",
        "username": "member:414777096",
        "profile_image": {
          "url": "https://media.licdn.com/dms/image/v2/D4E03AQFjX-BmKVA8sw/profile-displayphoto-shrink_100_100/profile-displayphoto-shrink_100_100/0/1681174265529?e=1756339200&v=beta&t=dVOLCn5tuwDg8ZPO4N5lbb4gsKyKUjRDoZGp62QMi2I",
          "width": 100,
          "height": 100,
          "expiresAt": 1756339200000
        }
      },
      "tags": null,
      "is_read": false,
      "is_archived": false,
      "posted_at": "2025-06-06T12:33:08.27",
      "imported_at": "2025-06-13T06:13:08.373",
      "sort_index": "1749795181410"
    }
  ],
  "meta": {
    "next_cursor": "MTc1OTYxNw==",
    "limit": 1,
    "total_count": 396
  }
}

Response fields

Every response is wrapped in the same envelope: a status flag and a data payload, with meta present where there is pagination or a plan limit to report.

statusboolean

true on success. Every LinkedMash API response carries this envelope flag alongside data, so a client can branch on it without re-reading the HTTP status.

dataarray

The matching saved posts, newest first.

data[].post_idstring

The LinkedIn activity id. It is the last path segment of a LinkedIn permalink and the value you pass to GET /v1/bookmarks/{id} and to the label endpoints.

data[].post_details.textstring

The full post body as saved, including line breaks and emoji. Not truncated.

data[].post_details.linkstring

Canonical LinkedIn permalink for the post.

data[].post_details.posted_atstring

ISO-8601 timestamp of when the author published the post on LinkedIn.

data[].post_details.attachmentsarray

Media and link previews carried by the post. Each entry has a type (image, video, article, document), a title, a url, and a thumbnail array of sized image variants.

data[].author_idstring

LinkedIn identifier for the author, stable across posts, so you can group a library by writer.

data[].author_details.namestring

The author's display name.

data[].author_details.usernamestring

The public LinkedIn vanity handle, i.e. the /in/ segment of their profile URL.

data[].author_details.biostring

The headline shown under the name on LinkedIn. This is the field most integrations use to infer role and company.

data[].author_details.profile_imageobject

url, width, height and expiresAt. The URL is a LinkedIn CDN address that stops resolving after expiresAt — re-host it if you need it to persist.

data[].tagsarray

The label names you have applied to this post. Empty when the post is untagged.

data[].is_readboolean

Whether the post has been marked read in LinkedMash. Both read and unread posts are returned unless you set is_unread_only.

data[].is_archivedboolean

Whether the post has been archived. Archived posts are hidden from list results by default.

data[].posted_atstring

Same as post_details.posted_at, lifted to the top level for sorting. This is the field to sort on — not created_at, which reflects when the record entered LinkedMash.

data[].imported_atstring

ISO-8601 timestamp of when LinkedMash first pulled the post into the library.

data[].sort_indexstring

Opaque ordering key. The encoded form of the last item’s sort_index is exactly what meta.next_cursor contains.

meta.next_cursorstring | null

Pass back as ?cursor= for the next page. Present only when the response returned exactly `limit` items; null means you have reached the end, and it is always null on a free-tier response.

meta.limitinteger

The limit actually applied, after clamping. Compare it with what you sent to detect the 100 ceiling or the free-tier cap of 5.

meta.total_countinteger

Total posts matching the filter, ignoring pagination.

meta.subscriptionobject

Present only on free-tier responses, as { limited: true, reason: "FREE_TIER_LIMIT" }. Its absence is how you tell a genuinely short library from a truncated one.

Mirroring a saved-posts library

The common job is not one call — it is walking the whole library once, then keeping it current. This is the sequence.

1

Size the job first

Call GET /v1/bookmarks/count with the same filters you plan to page. It costs one request and tells you whether the sync is 40 posts or 4,000, which is the difference between doing it inline and doing it on a queue.

2

Page with the cursor until it runs out

Request with limit=100, read meta.next_cursor, and pass it back as ?cursor= on the next call. Stop when next_cursor is null. Do not stop on a short page — a page can be short and still have a cursor.

3

Store sort_index, not your own counter

Each post carries sort_index, and the cursor is derived from the last item's. Persisting it means a resumed sync picks up exactly where it stopped rather than re-walking from the top.

4

Keep it current with bookmarked_from

For incremental runs, pass bookmarked_from set to the timestamp of your last successful sync. That returns only what has been saved since, which on most accounts is a single small page.

5

Handle the free-tier shape explicitly

If meta.subscription is present the response was truncated to 5 with no cursor. Treat that as 'cannot sync' and say so, rather than writing 5 posts and reporting success.

Behaviour and limits

Page with the cursor, never with an offset

A response only includes next_cursor when it returned exactly `limit` items, so an empty next_cursor is the end of the list rather than a hint to try again. Pass it back as ?cursor= to get the next page. Combine it with sort_cursor when you also set sort_by, otherwise ordering can drift between pages.

Free accounts are capped at 5 and cannot page

Without an active subscription the limit is forced to 5, next_cursor is nulled, has_more is false, and meta.subscription is set to { limited: true, reason: "FREE_TIER_LIMIT" }. Sending an explicit limit or cursor on a free key returns 402 with code PAGINATION_NOT_ALLOWED rather than silently truncating.

Filtering is a paid capability; keyword search is not

media_type, author, tag, posted_from, posted_to, bookmarked_from, bookmarked_to and comment all return 402 with code FILTER_NOT_ALLOWED on a free key. Plain listing and q search still work, which is what makes q the right first call when you do not know the plan behind a key.

Paid limits: 20 by default, 100 max, 50 on semantic search

limit defaults to 20 and is clamped to 100. The vector_search_term path is clamped lower, to 50, and returns has_more: false because semantic results are ranked rather than paged.

media_type accepts image, video and pdf only

Anything else — including 'article' — is rejected with 400 before the query runs. comment accepts only 'only' (saved comments) or 'exclude' (top-level posts). limit must be a positive integer.

Semantic search needs a vector store

vector_search_term is paid-only (402 VECTOR_SEARCH_NOT_ALLOWED on free). If the account has never initialised a vector store the call returns 500 with "Vector Store Not Setup" — run the setup step once and the same request starts working.

Errors

Failures carry the same envelope as a success, with the reason in message and a stable machine-readable code where the status alone is ambiguous — several distinct conditions share a 402.

Status
Meaning
400Bad Request
Invalid media_type, invalid comment value, a non-positive limit, or a cursor that could not be decoded.
401Unauthorized
The Authorization header is missing or the key is not a valid LinkedMash API key. Create or rotate keys on the API page in your LinkedMash settings.
402Payment Required
Code FILTER_NOT_ALLOWED, PAGINATION_NOT_ALLOWED or VECTOR_SEARCH_NOT_ALLOWED — a free key reached for a paid capability.
500Server Error
Something failed on our side. The body carries message "Failed to list bookmarks". Retry once; if it persists the request is not the problem.