Facebook Post URL Formats: Which Links Work (and Why One Post Has Two IDs)
Ask three people on your team for the link to the same Facebook post and you can easily get three different URLs. One copied it from the desktop site. One shared it from their phone. One clicked through from a notification and ended up with something containing permalink.php.
They all point at the same post. What you actually need to know is whether your tools agree on that, and which version to store.
We took one public post from NASA's Facebook page and sent it to /facebook/post four different ways. The good news came quickly. The surprise came from the IDs.
Four links, one post
The post was a short video, so NASA's page feed listed it as a reel. Here's what we sent:
| Format | URL we sent | Result |
|---|---|---|
| Reel link, as the feed gave it | facebook.com/reel/1134423309335584/ | Worked |
| Mobile | m.facebook.com/reel/1134423309335584/ | Worked |
| Permalink | facebook.com/permalink.php?story_fbid=1643634053798631&id=100044561550831 | Worked |
| Page posts path | facebook.com/nasa/posts/1643634053798631 | Worked |
All four came back as the same post, with the same 1,385 reactions. So the common formats all resolve, mobile links included, and you don't need to fight your team over which one they paste.
The catch: the ID depends on the link you send
Look closely at the numbers in those URLs. There are two different IDs in play for this one post.
1134423309335584 belongs to the reel, the video itself. 1643634053798631 belongs to the post, the item on the page's timeline that holds the video. The page feed handed us both: the post's id field was 1643634053798631, and its url pointed at the reel.
Then it got interesting. Send the reel link, and the response's post_id came back as 1643634053798631. Send the permalink or the /posts/ link, and post_id came back as 1134423309335584. Same post, same two numbers, swapped round depending on which door you knocked on.
Here's why that matters. If you deduplicate on post_id alone, this one post will appear twice in your data the first time two people submit it in two formats. Your counts drift, your reports double up, and nothing errors to warn you.
What we'd do instead: store every ID you see for a post, both the post_id in the response and the number in the URL, and treat a match on either as the same post.
Building the other formats yourself
You don't need to collect every variant from users. Two numbers from the page feed are enough to build the rest:
- the post's
id(1643634053798631here) - the page's ID, from the post's
author.id(100044561550831for NASA)
With those you can construct permalink.php?story_fbid=POST_ID&id=PAGE_ID and /PAGENAME/posts/POST_ID, and both worked in our test. That's handy when all you have is an ID in a database and you need a link a person can click.
Be careful not to mix those two numbers up when you pull them out of a URL. The id in a permalink is the page, not the post. Treat it as a post ID and every post from that page will look like a duplicate of every other.
Clean links before you store them
Copied Facebook links tend to drag tracking junk along: parameters like __cft__, __tn__, mibextid and rdid. None of them identify the post, but they make two copies of the same link look different to anything doing a string comparison. Strip them. Keep the parameters that do identify the post, like story_fbid and id on a permalink.
Host names matter too. Mobile m.facebook.com links worked fine. The short fb.com domain is another story. When we tried it for page lookups, the API rejected it outright. Rewrite everything to www.facebook.com and the question never comes up. We cover that rule, and others like it, in why an API says an account doesn't exist.
Here's a normaliser that does both jobs, plus a fetch that records every ID it sees:
const API = "https://api.sociavault.com/v1/scrape";
const KEY = process.env.SOCIAVAULT_API_KEY;
const KEEP = new Set(["story_fbid", "id", "fbid", "v"]); // parameters that identify the post
function cleanFacebookUrl(input) {
const u = new URL(String(input).trim());
u.hostname = "www.facebook.com"; // m.facebook.com and fb.com both become www
for (const key of [...u.searchParams.keys()]) {
if (!KEEP.has(key)) u.searchParams.delete(key);
}
u.hash = "";
return u.toString();
}
// Post IDs from a URL. Deliberately skips "id", which is the page, not the post.
function idsFromUrl(url) {
const u = new URL(url);
const ids = [];
const story = u.searchParams.get("story_fbid");
if (story) ids.push(story);
const m = u.pathname.match(/\/(?:reel|posts|videos)\/(\d+)/);
if (m) ids.push(m[1]);
return ids;
}
async function fetchPost(link) {
const url = cleanFacebookUrl(link);
const res = await fetch(`${API}/facebook/post?${new URLSearchParams({ url })}`, {
headers: { "x-api-key": KEY },
});
const body = await res.json();
if (body?.error) return { url, error: body.error };
const d = body.data ?? {};
const ids = new Set([d.post_id, ...idsFromUrl(url)].filter(Boolean).map(String));
return {
url,
ids: [...ids], // two records are the same post if these overlap
likes: d.like_count ?? null,
comments: d.comment_count ?? null,
shares: d.share_count ?? null,
text: d.description ?? "",
};
}
The same in Python:
import os, re, requests
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
API = "https://api.sociavault.com/v1/scrape"
HEADERS = {"x-api-key": os.environ["SOCIAVAULT_API_KEY"]}
KEEP = {"story_fbid", "id", "fbid", "v"}
def clean_facebook_url(link):
parts = urlsplit(link.strip())
query = urlencode([(k, v) for k, v in parse_qsl(parts.query) if k in KEEP])
return urlunsplit(("https", "www.facebook.com", parts.path, query, ""))
def ids_from_url(url):
parts = urlsplit(url)
ids = [v for k, v in parse_qsl(parts.query) if k == "story_fbid"] # not "id": that's the page
m = re.search(r"/(?:reel|posts|videos)/(\d+)", parts.path)
if m:
ids.append(m.group(1))
return ids
def fetch_post(link):
url = clean_facebook_url(link)
body = requests.get(f"{API}/facebook/post", params={"url": url}, headers=HEADERS, timeout=90).json()
if body.get("error"):
return {"url": url, "error": body["error"]}
d = body.get("data") or {}
ids = {str(i) for i in [d.get("post_id"), *ids_from_url(url)] if i}
return {"url": url, "ids": sorted(ids), "likes": d.get("like_count"),
"comments": d.get("comment_count"), "text": d.get("description", "")}
When you save a post, check whether any of its ids already exists in your table. If one does, it's the post you already have, whatever link it arrived through.
Formats we didn't test
We stuck to formats we could construct and check. Others exist, and you'll meet them:
- Share links, like
facebook.com/share/p/...andfacebook.com/share/r/..., which the share button generates fb.watchshort links for videospfbidpost IDs, long strings startingpfbid0that appear in some post URLs instead of a number- Photo links with
photo.php?fbid= - Group posts, at
/groups/GROUP_ID/posts/POST_ID/
Share links and fb.watch links are redirects. Open one in a browser and it lands on a full URL, and that's the one to send and store. Group posts belong to groups, which have their own feed endpoint. We covered those in scraping public Facebook groups.
Limits worth knowing
- Public posts only. Posts on private profiles or in private groups aren't available, whatever the link looks like.
- We tested one post. It was a video post, in four formats. Text and photo posts may report their IDs differently, so check a sample of your own before you rely on any one pattern.
- Numbers move. Reactions and comments change constantly, so a stored count is a snapshot of the moment you fetched it.
- Facebook changes its URLs. New formats appear every year or two. Log any link your normaliser can't parse rather than guessing.
Frequently Asked Questions
Which Facebook post URL should I store?
Store the cleaned www.facebook.com version of whatever link you have, plus every ID you've seen for the post. The link is for people to click. The IDs are what you match on.
Why does the same Facebook post have two IDs?
A video post has an ID for the post on the timeline and another for the video itself. In our test, which one came back as post_id depended on which kind of link we sent.
Do m.facebook.com links work?
Yes. A mobile reel link returned the same post as the desktop one. We'd still rewrite to www.facebook.com so every stored copy matches.
What is a permalink.php link?
It's a direct link to a post, built from the post ID (story_fbid) and the page ID (id). You can construct one yourself from a post's ID and its author's ID.
How do I handle Facebook share links?
Share links and fb.watch links redirect to a full post URL. Open the link in a browser, copy the address it lands on, and store and send that.
Can I get posts from private profiles or groups?
No. Only posts that are publicly visible can be fetched.
Have a spreadsheet of Facebook links in every format? Sign up for 50 free credits, run them through the normaliser, and find out how many are really the same post.
Found this helpful?
Share it with others who might benefit
Ready to Try SociaVault?
Start extracting social media data with our powerful API. No credit card required.