API documentation
Read the webmentions this service has collected for your pages, show them on your site, follow them as a feed, or take everything with you.
Basics
Every endpoint is under https://webmention.io/api/ and answers GET requests. Responses are JSON unless you ask for another format by extension.
The API sends Access-Control-Allow-Origin: *, so you can call it from a browser as well as from a server.
Formats
| Path | Format |
|---|---|
/api/mentions.jf2 | jf2. The recommended format, and the one used everywhere in these docs and in web hook payloads. |
/api/mentions or .json | The original JSON format, kept for existing clients. Same query parameters. /api/links is an alias. |
/api/mentions.atom | An Atom feed. See Feeds. |
/api/mentions.html | A Microformats h-feed you can follow in a reader. |
Your token
Queries for a single page (by target) are public and need nothing else. Queries for a whole site or account need the token shown on your Settings page.
Send it as a query parameter or, to keep it out of URLs and access logs, as a header:
GET https://webmention.io/api/mentions.jf2?domain=example.com&token=xxxxx
GET https://webmention.io/api/mentions.jf2?domain=example.com
Authorization: Bearer xxxxx
The token can only read the mentions sent to your sites. It cannot change anything on your account.
Private webmentions are only ever included in token-authenticated listings; public queries by target never return them.
Which URL a mention is filed under
A mention is filed under the target's canonical URL. When a target is first seen, the service fetches it, follows your site's redirects and honours its rel="canonical",
and files the mention under the URL it ends up at, as long as that URL is on one of your sites. A #fragment in the target is ignored.
Every other form that led to the page is remembered as an alias, so target= queries for any of them return the same mentions, and wm-target is always the canonical URL.
A #fragment does not change which page a mention belongs to, but it is kept: see wm-fragment under Fields on a mention.
Trailing slashes and http/https are not treated as equivalent by rule; your site decides, by redirecting.
If you move a page later, use "Moved a page?" on the Sites page to re-file its mentions once the old URL redirects.
Show mentions on your page
The quickest way to show what people have said about a page is the rendering script. It has no dependencies and works on any static site. Likes, reposts and bookmarks become a row of avatars; replies, mentions and RSVPs become a list with the author, what they wrote, and a link to the original.
<div data-webmention-target="https://example.com/post/"></div>
<link rel="stylesheet" href="https://webmention.io/assets/webmention-render.css">
<script src="https://webmention.io/js/webmention-render.js" defer></script>
Put the div where the responses should appear. Nothing is added to the page when there are no mentions.
Everything is built from the API data with DOM calls, never from HTML strings, so nothing in a mention can add markup to your page.
Options
All options are attributes on the div.
| Attribute | Meaning |
|---|---|
data-webmention-target | The page to show mentions of. Leave the value empty to use the current page's URL, which suits a template used on every post. |
data-webmention-api | The feed to read. Defaults to https://webmention.io/api/mentions.jf2. Point it at https://webmention.io/api/example/mentions.jf2 while developing to see every kind of mention before your site has received any. |
data-webmention-html | Show each mention's content.html instead of its plain text. The HTML was sanitised by webmention.io, but it is still someone else's markup, so this is off unless you add the attribute. |
data-webmention-limit | How many mentions to fetch, oldest first. Default 100, at most 1000. |
Live demo
This is the script running on this page against the example feed:
<div data-webmention-target="https://example.com/post"
data-webmention-api="https://webmention.io/api/example/mentions.jf2"></div>
Styling
The stylesheet is small and everything in it is under .webmentions, so override what you like or leave the stylesheet out and write your own.
The classes are .wm-title, .wm-facepile and .wm-heading, .wm-faces/.wm-face/.wm-avatar for the avatar rows,
and .wm-replies/.wm-reply with .wm-reply-head, .wm-author, .wm-verb, .wm-permalink, .wm-name and .wm-content for the list.
Or roll your own
If you would rather control the markup, the API is easy to call directly. To show a count:
fetch("https://webmention.io/api/count?target=https://example.com/post/")
.then(response => response.json())
.then(data => console.log(data.count, data.type));
To list the mentions themselves, with the fields described under List mentions:
fetch("https://webmention.io/api/mentions.jf2?target=https://example.com/post/&sort-dir=up")
.then(response => response.json())
.then(feed => feed.children.forEach(mention => console.log(mention.author.name, mention["wm-property"])));
Set textContent rather than innerHTML when you put a mention's fields on your page; only content.html is meant to be inserted as markup, and only if you choose to.
The older jsonp parameter is still supported for pages that cannot use fetch.
List mentions
GET https://webmention.io/api/mentions.jf2?target=https://example.com/post/
Returns the mentions of one page, of several pages, of a whole site, or of everything on your account, depending on the parameters.
Parameters
| Parameter | Meaning |
|---|---|
target | The page to list mentions of. Repeat as target[] to combine several pages, up to 50, which is useful when a post has had more than one URL. A target with no scheme, such as //example.com/post, matches both http and https. Include a #fragment to get only the mentions sent to that fragment; leave it out for all of them. |
domain | Every mention of one of your sites. Needs your token. |
token | Your token. With no target and no domain, lists everything on your account. |
wm-property | Only mentions of one kind: in-reply-to, like-of, repost-of, bookmark-of, mention-of or rsvp. Repeat as wm-property[] for several. mention-of matches everything that is not one of the other kinds, including older mentions stored without a type; rsvp matches every RSVP value. |
since | Only mentions received after this time, such as 2026-06-01T10:00:00-0700. This is when webmention.io received the mention, not the date the post reports. |
since_id | Only mentions with a wm-id greater than this. The simplest way to poll for new mentions: remember the largest id you have seen. |
sort-by | created (default, when it was received), updated, published (the date the linking page reports, falling back to created), or rsvp (no, interested, maybe, yes). |
sort-dir | down (default, newest first) or up (oldest first). |
per-page | How many to return. Default 20, at most 1000. |
page | Which page of results, counting from 0. |
jsonp | Wrap the response in a call to this function. |
Response
A jf2 feed. The mentions are in children, and paging says how many there are in all so you know whether to ask for another page.
{
"type": "feed",
"name": "Webmentions",
"children": [
{
"type": "entry",
"author": {
"type": "card",
"name": "Tantek Çelik",
"url": "http://tantek.com/",
"photo": "http://tantek.com/logo.jpg"
},
"url": "http://tantek.com/2013/112/t2/milestone-show-indieweb-comments-h-entry-pingback",
"published": "2013-04-22T15:03:00-07:00",
"wm-received": "2013-04-25T17:09:33Z",
"wm-id": 900,
"wm-source": "http://tantek.com/2013/112/t2/milestone-show-indieweb-comments-h-entry-pingback",
"wm-target": "https://indieweb.org/",
"wm-protocol": "webmention",
"content": {
"html": "Another milestone: <a href=\"https://twitter.com/eschnou\">@eschnou</a> automatically shows #indieweb comments…",
"text": "Another milestone: @eschnou automatically shows #indieweb comments…"
},
"mention-of": "https://indieweb.org/",
"wm-property": "mention-of",
"wm-private": false
}
],
"paging": {
"per-page": 20,
"page": 0,
"total": 93,
"total-pages": 5
}
}
Fields on a mention
Fields starting with wm- come from webmention.io; the rest are the h-entry properties parsed from the linking page, so they are present only when the page provides them.
| Field | Meaning |
|---|---|
wm-id | A stable numeric id, increasing over time. Use it with since_id. |
wm-source | The URL the webmention was sent from: the page that links to yours. |
wm-target | The canonical URL of your page that was linked to. |
wm-property | What kind of mention this is: in-reply-to, like-of, repost-of, bookmark-of, rsvp or mention-of. The same name also appears as a property holding your URL, so an entry with "like-of": "https://example.com/post/" is a like of that post. |
wm-received | When webmention.io received it, in UTC. |
wm-private | true for a private webmention. Only present in token-authenticated listings. |
wm-protocol | webmention or pingback. |
wm-fragment | The #fragment of your page this one was sent to, without the hash, when it had one. Ask for target=…#that-fragment to get only those. Mentions received before September 2026 have none recorded. |
author | An h-card with name, url and photo, any of which may be empty. |
url | The post's own permalink. Usually the same as wm-source, but differs for bridged posts, for example a reply sent on behalf of a social media post. |
published | The date the post reports, as written, with its own timezone. |
content | An object with text and, when the post had markup, sanitised html. Very long content is cut short when it is stored. |
name | The post's title, when it has one. Notes usually have none. |
summary | The post's summary as {"content-type": "text/plain", "value": "…"}, when it has one. |
rsvp | yes, no, maybe or interested, on RSVPs. |
photo, video, audio | Media attached to the post, as lists of URLs. |
syndication | Other copies of the post, as a list of URLs. |
To walk through everything, either step page from 0 to total-pages - 1, or fetch with sort-dir=up and repeat with since_id set to the last wm-id you saw. The second way is stable while new mentions arrive.
The example feed returns one of every shape a mention can take, so you can test your code against all of them.
Count mentions
GET https://webmention.io/api/count?target=https://example.com/post/
{
"count": 6,
"type": {
"like": 2,
"mention": 2,
"reply": 1,
"rsvp-yes": 1
}
}
The total number of mentions of a page, and the count of each kind. The keys are the short type names: like, repost, bookmark, reply, mention and rsvp-yes, rsvp-no, rsvp-maybe and rsvp-interested.
target and target[] work as on List mentions. Private webmentions are not counted.
Feeds
Everything on your account is also available as a feed you can subscribe to, so new mentions arrive in your reader. Both URLs, with your token filled in, are on the Settings page.
GET https://webmention.io/api/mentions.atom?token=xxxxx
GET https://webmention.io/api/mentions.html?token=xxxxx
Each Atom entry carries the author's name and URL, a link to the mention, its published date, and the mention's content as HTML, falling back to its summary or title.
The h-feed version shows the same in a page marked up with Microformats, for readers such as Aperture.
Every parameter from List mentions works on both, so a feed of just the replies to one post is /api/mentions.atom?target=…&wm-property=in-reply-to.
Example data
GET https://webmention.io/api/example/mentions.jf2?target=https://example.com/post
GET https://webmention.io/api/example/count
Made-up mentions on .example domains, one of each shape the real feed produces: replies, likes, reposts, bookmarks, plain mentions, all four RSVP values, an invite, photo, video and audio posts,
a private webmention, a pingback, a legacy mention with no type or author, the pre-2018 content-type/value content shape, a check-in, syndication links, a bridged reply whose url differs from wm-source, and long HTML with non-Latin text.
Use it to build and test before your site has received anything.
target is echoed as wm-target. wm-property, sort-dir, per-page, page and jsonp work as on the real feed, and the response carries paging.
Names are randomised on every request; add seed=123 to get the same ones again. The wm-id values 1001 to 1021 are stable, one per case.
Export
GET https://webmention.io/api/export.jf2?token=xxxxx
GET https://webmention.io/api/export.jf2?token=xxxxx&domain=example.com
Every published mention on your account, or on one site, as a single jf2 feed in the same shape as List mentions: oldest first, private mentions included, mentions you have deleted or that are awaiting moderation left out.
The response is streamed as a file download named webmentions-example.com-2026-09-14.jf2.json, with one mention per line inside the children array so the file can be read line by line as well as parsed whole.
An export reads everything on the account, and for a long-lived site that can be hundreds of megabytes, so it can be started once every five minutes per account. A second attempt inside that window gets a 429 with a Retry-After header.
It is meant for backups and for moving to another service; to keep a copy up to date, poll List mentions with since_id instead.
Deleted mentions
GET https://webmention.io/api/deleted.jf2?target=https://example.com/post/
GET https://webmention.io/api/deleted.jf2?token=xxxxx
Mentions that have been removed, newest first, whether because the linking page stopped linking to you, because you deleted or rejected them on the dashboard, or because you blocked their source.
Each item is {"wm-id", "wm-source", "wm-target", "wm-deleted"}. since, since_id, per-page and page work as on List mentions.
A client that keeps its own copy of its mentions can poll this to prune them.
Web hooks
Instead of polling, give a site a callback URL on its settings page under Sites, and every webmention that verifies is POSTed to it as JSON as it arrives. Webmentions that fail verification, and ones held for moderation until you approve them, are not sent.
The post is the same jf2 entry the API returns, so one parser handles both.
POST https://example.com/webmention/hook
Content-Type: application/json
X-Webmention-Signature: sha256=2f7e…
{
"secret": "1234abcd",
"source": "http://rhiaro.co.uk/2015/11/1446953889",
"target": "http://aaronparecki.com/notes/2015/11/07/4/indiewebcamp",
"private": false,
"post": {
"type": "entry",
"author": {
"type": "card",
"name": "Amy Guy",
"photo": "https://avatars.webmention.io/rhiaro.co.uk/829d3f6e7083d7ee8bd7b20363da84d88ce5b4ce094f78fd1b27d8d3dc42560e.png",
"url": "http://rhiaro.co.uk/about#me"
},
"url": "http://rhiaro.co.uk/2015/11/1446953889",
"published": "2015-11-08T03:38:09+00:00",
"wm-received": "2015-11-08T03:40:12Z",
"wm-id": 900,
"wm-source": "http://rhiaro.co.uk/2015/11/1446953889",
"wm-target": "http://aaronparecki.com/notes/2015/11/07/4/indiewebcamp",
"wm-protocol": "webmention",
"name": "repost of http://aaronparecki.com/notes/2015/11/07/4/indiewebcamp",
"repost-of": "http://aaronparecki.com/notes/2015/11/07/4/indiewebcamp",
"wm-property": "repost-of",
"wm-private": false
}
}
wm-property, and the matching property inside post, says what kind of post it is: in-reply-to, like-of, repost-of, bookmark-of, mention-of or rsvp, as described under Fields on a mention.
private is true for a private webmention.
Checking it came from webmention.io
If the site has a callback secret, it is sent as secret in the body and the request also carries X-Webmention-Signature: sha256=<hex>, the HMAC-SHA256 of the raw request body keyed with that secret.
Compare the signature rather than the secret, and your endpoint never has to read the body before trusting it.
Deletions
When a webmention is later deleted, because the linking page was removed or stopped linking to you, or because you deleted or rejected it on the dashboard, the callback receives the same source and target with deleted and no post:
{
"secret": "1234abcd",
"source": "http://rhiaro.co.uk/2015/11/1446953889",
"target": "http://aaronparecki.com/notes/2015/11/07/4/indiewebcamp",
"private": false,
"deleted": true
}
Retries
Your endpoint should answer with a 2xx status within 20 seconds. A delivery it did not take, because it did not answer, or answered with a 5xx, 408 or 429, is tried again
after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then given up. Any other 4xx is taken as a refusal and is not retried.
Retries carry the same body, signed with the site's secret at the time of sending. Delivery is therefore at least once: an endpoint that took a delivery but
answered too slowly can receive it again, so treat post.wm-id (or source and target for a deletion) as the key to update rather than insert.
Retries stop if you change or clear the callback URL, or archive or delete the site.
Each site's settings page lists its last 50 deliveries with the status or error, the attempt number, the time taken, and the request and response bodies, and can re-send any of them or send the newest webmention as a test.
If your endpoint was down for longer than the retries cover, fetch what it missed from List mentions with since_id and from Deleted mentions; both return the same data the web hook carries.