Slack Message Permalink: Format, chat.getPermalink and ts, Tested
A Slack message link is /archives/<channel>/p<ts without the dot>. We copied links in Slack web and called chat.getPermalink on 1 October 2026 to compare the formats for channel messages and thread replies, then turned links back into a channel and ts.
On this page
A Slack message link has the form https://<workspace>.slack.com/archives/<channel ID>/p<ts>, where the part after p is the message's ts with the dot removed. A link to a thread reply adds ?thread_ts=<parent ts>&cid=<channel ID>. You get one from the message menu (More actions > Copy link) or from the API with chat.getPermalink. We did both on 1 October 2026 in a test workspace, for a channel message, a thread parent and a thread reply, and the two ways do not always return the same string.
The link format, decoded
These are the links Slack gave us for three messages in one channel, C0C5QDNUJ7M:
| Message | Copy link in Slack web | chat.getPermalink |
|---|---|---|
| Message with no replies | /archives/C0C5QDNUJ7M/p1790838379966019 | same |
| Thread parent (1 reply) | /archives/C0C5QDNUJ7M/p1790838372599559 | same, plus ?thread_ts=1790838372.599559&cid=C0C5QDNUJ7M |
| Reply in that thread | /archives/C0C5QDNUJ7M/p1790838372960849?thread_ts=1790838372.599559&cid=C0C5QDNUJ7M | same |
To read a link:
- •
C0C5QDNUJ7Mis the channel ID. How to find a Slack channel ID covers the other places it shows up. - •
p1790838372960849is the messagets1790838372.960849. Put the dot back after the 10th digit. The first 10 digits are a Unix time in seconds; paste them into the Slack timestamp tool to see the date. - •
thread_tsis thetsof the thread's first message, andcidrepeats the channel ID.
The one difference we saw: on a thread parent, chat.getPermalink added thread_ts (equal to the parent's own ts) while Copy link did not. Both links open the same message, so do not compare permalinks as strings to decide whether two links point at the same message. Compare channel and ts.
Get a permalink with chat.getPermalink
chat.getPermalink takes channel and message_ts. Our bot token had chat:write and channels:history; Slack's method reference lists no required scope. Real responses:
{"ok": true, "permalink": "https://slack-0yr1948.slack.com/archives/C0C5QDNUJ7M/p1790838379966019", "channel": "C0C5QDNUJ7M"}
{"ok": true, "permalink": "https://slack-0yr1948.slack.com/archives/C0C5QDNUJ7M/p1790838372960849?thread_ts=1790838372.599559&cid=C0C5QDNUJ7M", "channel": "C0C5QDNUJ7M"}
The errors we got for wrong input:
message_ts or channel we sent | Response |
|---|---|
The p-number from the link, p1790838379966019 | message_not_found |
The ts without its dot, 1790838379966019 | message_not_found |
| A ts with the right seconds but wrong decimals | message_not_found |
| A channel ID that does not exist | channel_not_found |
So message_not_found usually means the ts is not the exact string with its dot and six decimals. If you store ts as a float, you lose digits and get this error.
Posting links is also the way to move many messages at once, because Slack forwards only one message at a time. In our forward multiple messages test, one message with 10 links showed a preview for only the first 5.
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Turn a permalink back into channel and ts
This is the parser, then the script we ran against the three API links:
import re
from urllib.parse import urlparse, parse_qs
def parse_permalink(url):
u = urlparse(url)
m = re.search(r"/archives/([A-Z0-9]+)/p(\d{16})", u.path)
channel, digits = m.group(1), m.group(2)
ts = digits[:10] + "." + digits[10:]
thread_ts = parse_qs(u.query).get("thread_ts", [None])[0]
return channel, ts, thread_ts
Fetching the message needs two different calls. A top-level message comes from conversations.history with latest set to its ts. A thread reply only comes from conversations.replies:
def fetch_message(url):
channel, ts, thread_ts = parse_permalink(url)
if thread_ts and thread_ts != ts:
r = client.conversations_replies(channel=channel, ts=thread_ts, latest=ts, inclusive=True, limit=1)
return [m for m in r["messages"] if m["ts"] == ts][0]
r = client.conversations_history(channel=channel, latest=ts, inclusive=True, limit=1)
return r["messages"][0]
Output with a slack_sdk bot client:
('C0C5QDNUJ7M', '1790838372.599559', '1790838372.599559')
-> 1790838372.599559 'Permalink test: parent message'
('C0C5QDNUJ7M', '1790838372.960849', '1790838372.599559')
-> 1790838372.960849 'Permalink test: reply in the thread'
('C0C5QDNUJ7M', '1790838379.966019', None)
-> 1790838379.966019 'Permalink test: message with no replies'
history with the reply ts -> 1790838372.599559 'Permalink test: parent message'
The last line is the trap. We called conversations.history with the reply's ts and it returned the thread parent, with ok: true and no warning, because the channel history holds only top-level messages. If your code "fetches the wrong message" from a reply link, this is why. A reply link copied without its ?thread_ts= part has the same problem: the parser cannot tell it is a reply. Reading whole channels and threads is covered in Slack API: get messages from a channel, and posting into a thread in Slack thread_ts, tested.
FAQ
Does editing a message change its permalink?
No. The ts stays the same after an edit, so the link stays the same.
Can I get a permalink with a webhook?
No. An incoming webhook returns only ok, with no ts, so you cannot build the link. Post with chat.postMessage instead, which returns channel and ts, then call chat.getPermalink.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
Slack Daily Standup Without a Paid Bot: /remind, Workflow Builder and a Script
We set up a weekday 9:30 standup in a free Slack workspace three ways: a /remind reminder, Workflow Builder, and a short script that posts the questions and collects the thread replies.
Slack Tabs Under a Channel Name: What Each One Is and How to Edit Them
We added, renamed, moved, hid and deleted the tabs under a channel name in Slack web on the Free plan, and compared them with the tabs in a DM.
Leave Multiple Slack Channels at Once: 4 Ways Timed, Plus a Script
Slack has no bulk leave button. We made 12 test channels and left them with the sidebar menu, the channel browser, /leave and a 30-line script, and timed each way.