WP Annotations

Back-end annotations implemented as a custom comment type. Conforms to the W3C annotation data model for annotation selectors and maintains some parity with the W3C annotation object model and protocol — while still doing things the WordPress way, which maximizes compatibility with WP REST API utilities, including those in JavaScript.
Create Annotation
POST /wp-json/wp/v2/annotations

Create a new annotation.

Request body

Object
properties
post
integer required

Post ID.

parent
integer

Parent ID, if it’s a reply.

Default:
0
author
integer

Author’s user ID. Requires ability to edit_others_annotations. Default is that of the authenticated user.

Example:
1
author_name
string

Author’s name. Default is that of the authenticated user.

Example:
John Smith
author_email
string

Author’s email address. Default is that of the authenticated user.

Example:
john@example.com
author_url
string

Author’s URL. Default is that of the authenticated user.

Example:
https://john.example.com/
author_ip
string

Author’s IP address. Requires ability to edit_others_annotations. Default is auto-detected using $_SERVER['REMOTE_ADDR'].

Example:
127.0..0.1
author_user_agent
string

Author’s user-agent (browser). Requires ability to edit_others_annotations. Default is auto-detected using $_SERVER['HTTP_USER_AGENT'].

Example:
Mozilla/5.0
content
string required

Comment content. Back-end annotations are not spam or flood checked. However, they do have a maximum length.

Min length: 1
Example:
Hello world!
via
string
Max length: 250
Pattern: [a-zA-Z0-9:_-]+
Example:
gutenberg
selector
Object

W3C annotation selector. Max JSON-encoded size is 16kb. SVG selectors allow up to 128kbs, but those are currently disabled, pending a security review. Regarding selectors in JavaScript, see suggestion here: https://jsfiddle.net/jaswrks/1cLaeyfd/

Examples:
{
    "type": "CssSelector",
    "value": "#foo > .bar"
}
{
    "type": "CssSelector",
    "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
    "refinedBy": {
          "type": "TextPositionSelector",
          "start": 412,
          "end": 667
    }
}
{
    "type": "RangeSelector",
    "startSelector": {
        "type": "XPathSelector",
        "value": "//*[@id='c9xi4jhi']/text()[1]",
        "refinedBy": {
            "type": "TextPositionSelector",
            "start": 0,
            "end": 0
        }
    },
    "endSelector": {
        "type": "XPathSelector",
        "value": "//*[@id='c9xi4jhi']/text()[1]",
        "refinedBy": {
            "type": "TextPositionSelector",
            "start": 100,
            "end": 100
        }
    }
}
type
string

Type of selector.

Enumeration:
FragmentSelector
CssSelector
XPathSelector

Can select text nodes!

TextQuoteSelector
TextPositionSelector
DataPositionSelector
SvgSelector

Pending a security review.

RangeSelector
value
string

Required for CSS and XPath types. Other properties apply to other types.

status
string

The default status for a new annotation is approve. To create an annotation with a restricted status (hold, 0, spam, trash) you must be able to edit_others_annotations.

Enumeration:
hold

Held for moderation. Same as ‘0’.

approved

Same as ‘1’ and ‘approve’.

approve

Same as ‘1’ and ‘approved’.

resolve

Archived as resolved.

reject

Archived as rejected.

archive

Archived for another reason.

spam

Annotation spam.

trash

In the trash.

meta
Object

Any comment meta properties registered using register_meta().

Example:
{
    "my_key": "my value",
    "another_key": {
        "another": "value"
    }
}
pattern properties
[a-z0-9_-]+
Any of

Any value.

string
number
Array of unknown
Object
additional properties
Object
Examples

i.e., Also supports all the same arguments as the REST API for comments.

Example 1
Example 2
Example 3
Example 4
{
    "post": 1,
    "content": "Hello world!",
    "via": "gutenberg"
}
{
    "post": 1,
    "content": "Hello world!",
    "via": "gutenberg",
    "selector": {
        "type": "CssSelector",
        "value": "#foo > .bar"
    }
}
{
    "post": 1,
    "author": 1,
    "author_name": "John Smith",
    "author_email": "john@example.com",
    "author_url": "https://john.example.com/",
    "author_ip": "127.0..0.1",
    "author_user_agent": "Mozilla/5.0",
    "content": "Hello world!",
    "via": "gutenberg",
    "selector": {
        "type": "CSSSelector",
        "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
        "refinedBy": {
              "type": "TextPositionSelector",
              "start": 412,
              "end": 667
        }
    }
}
{
    "post": 1,
    "author": 1,
    "author_name": "John Smith",
    "author_email": "john@example.com",
    "author_url": "https://john.example.com/",
    "author_ip": "127.0..0.1",
    "author_user_agent": "Mozilla/5.0",
    "content": "Hello world!",
    "via": "gutenberg",
    "selector": {
        "type": "RangeSelector",
        "startSelector": {
            "type": "XPathSelector",
            "value": "//*[@id='c9xi4jhi']/text()[1]",
            "refinedBy": {
                "type": "TextPositionSelector",
                "start": 0,
                "end": 0
            }
        },
        "endSelector": {
            "type": "XPathSelector",
            "value": "//*[@id='c9xi4jhi']/text()[1]",
            "refinedBy": {
                "type": "TextPositionSelector",
                "start": 100,
                "end": 100
            }
        }
    },
    "meta": {
        "my_key": "my value",
        "another_key": {
            "another": "value"
        }
    }
}
Get Annotation
GET /wp-json/wp/v2/annotations/{id}

Get a single annotation by ID.

Path variables

id
integer required

Annotation ID.

Request parameters

context
string optional

REST API context.

Enumeration:
view
edit
embed
Default:
view
Example 1
Example 2

?context=view

{
    "id": 1,
    "post": 1,
    "parent": 0,
    "author": 1,
    "author_name": "John",
    "author_url": "https://example.john.com/",
    "date": "2018-01-11T11:09:03",
    "date_gmt": "2018-01-11T11:09:03",
    "content": {
        "rendered": "<p>Hello world!</p>\n"
    },
    "link": "https://example.com/hello-world/#comment-1",
    "status": "approved",
    "type": "annotation",
    "author_avatar_urls": {
        "24": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=24&d=mm&r=g",
        "48": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=48&d=mm&r=g",
        "96": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=96&d=mm&r=g"
    },
    "meta": [],
    "via": "gutenberg",
    "selector": {
        "type": "CssSelector",
        "value": "#foo > .bar"
    },
    "_links": {
        "self": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations/1"
            }
        ],
        "collection": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations"
            }
        ],
        "author": [
            {
                "embeddable": true,
                "href": "https://example.com/wp-json/wp/v2/users/1"
            }
        ],
        "up": [
            {
                "embeddable": true,
                "post_type": "post",
                "href": "https://example.com/wp-json/wp/v2/posts/1"
            }
        ]
    }
}

?context=edit

{
    "id": 1,
    "post": 1,
    "parent": 0,
    "author": 1,
    "author_name": "John",
    "author_email": "john@example.com",
    "author_url": "https://example.john.com/",
    "author_ip": "192.168.42.1",
    "author_user_agent": "Mozilla/5.0",
    "date": "2018-01-11T11:09:03",
    "date_gmt": "2018-01-11T11:09:03",
    "content": {
        "rendered": "<p>Hello world!</p>\n",
        "raw": "Hello world!"
    },
    "link": "https://example.com/hello-world/#comment-1",
    "status": "approved",
    "type": "annotation",
    "author_avatar_urls": {
        "24": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=24&d=mm&r=g",
        "48": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=48&d=mm&r=g",
        "96": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=96&d=mm&r=g"
    },
    "meta": [],
    "via": "gutenberg",
    "selector": {
        "type": "CssSelector",
        "value": "#foo > .bar"
    },
    "_links": {
        "self": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations/1"
            }
        ],
        "collection": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations"
            }
        ],
        "author": [
            {
                "embeddable": true,
                "href": "https://example.com/wp-json/wp/v2/users/1"
            }
        ],
        "up": [
            {
                "embeddable": true,
                "post_type": "post",
                "href": "https://example.com/wp-json/wp/v2/posts/1"
            }
        ]
    }
}
Update Annotation
PUT /wp-json/wp/v2/annotations/{id}

Update annotation properties.

Path variables

id
integer required

Annotation ID.

Request body

Object
properties
author
integer

Author’s user ID. Requires ability to edit_others_annotations.

Example:
1
author_name
string

Author’s name. Requires ability to edit_others_annotations.

Example:
John Smith
author_email
string

Author’s email address. Requires ability to edit_others_annotations.

Example:
john@example.com
author_url
string

Author’s URL. Requires ability to edit_others_annotations.

Example:
https://john.example.com/
author_ip
string

Author’s IP address. Requires ability to edit_others_annotations.

Example:
127.0..0.1
author_user_agent
string

Author’s user-agent (browser). Requires ability to edit_others_annotations.

Example:
Mozilla/5.0
content
string

Comment content. Back-end annotations are not spam or flood checked. However, they do have a maximum length.

Min length: 1
Example:
Hello world!
via
string
Max length: 250
Pattern: [a-zA-Z0-9:_-]+
Example:
gutenberg
selector
Object

W3C annotation selector. Max JSON-encoded size is 16kb. SVG selectors allow up to 128kbs, but those are currently disabled, pending a security review. Regarding selectors in JavaScript, see suggestion here: https://jsfiddle.net/jaswrks/1cLaeyfd/

Examples:
{
    "type": "CssSelector",
    "value": "#foo > .bar"
}
{
    "type": "CssSelector",
    "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
    "refinedBy": {
          "type": "TextPositionSelector",
          "start": 412,
          "end": 667
    }
}
{
    "type": "RangeSelector",
    "startSelector": {
        "type": "XPathSelector",
        "value": "//*[@id='c9xi4jhi']/text()[1]",
        "refinedBy": {
            "type": "TextPositionSelector",
            "start": 0,
            "end": 0
        }
    },
    "endSelector": {
        "type": "XPathSelector",
        "value": "//*[@id='c9xi4jhi']/text()[1]",
        "refinedBy": {
            "type": "TextPositionSelector",
            "start": 100,
            "end": 100
        }
    }
}
type
string

Type of selector.

Enumeration:
FragmentSelector
CssSelector
XPathSelector

Can select text nodes!

TextQuoteSelector
TextPositionSelector
DataPositionSelector
SvgSelector

Pending a security review.

RangeSelector
value
string

Required for CSS and XPath types. Other properties apply to other types.

status
string

To set a restricted status (hold, 0, spam, trash) you must be able to edit_others_annotations. However, if the authenticated user is the annotation author, and they can delete_annotation, trash becomes available.

Enumeration:
hold

Held for moderation. Same as ‘0’.

approved

Same as ‘1’ and ‘approve’.

approve

Same as ‘1’ and ‘approved’.

resolve

Archived as resolved.

reject

Archived as rejected.

archive

Archived for another reason.

spam

Annotation spam.

trash

In the trash.

meta
Object

Any comment meta properties registered using register_meta().

Example:
{
    "my_key": "my value",
    "another_key": {
        "another": "value"
    }
}
pattern properties
[a-z0-9_-]+
Any of

Any value.

string
number
Array of unknown
Object
additional properties
Object
Examples

i.e., Also supports all the same arguments as the REST API for comments, except that post and parent are currently readonly in annotation comment types.

Example 1
Example 2
Example 3
Example 4
Example 5
{
    "content": "Hello world!"
}
{
    "status": "resolve"
}
{
    "status": "reject"
}
{
    "status": "archive"
}
{
    "author": 1,
    "author_name": "John Smith",
    "author_email": "john@example.com",
    "author_url": "https://john.example.com/",
    "author_ip": "127.0..0.1",
    "author_user_agent": "Mozilla/5.0",
    "content": "Hello world!",
    "status": "approved",
    "via": "gutenberg",
    "selector": {
        "type": "CSSSelector",
        "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
        "refinedBy": {
              "type": "TextPositionSelector",
              "start": 412,
              "end": 667
        }
    },
    "meta": {
        "my_key": "my value",
        "another_key": {
            "another": "value"
        }
    }
}
Delete Annotation
DELETE /wp-json/wp/v2/annotations/{id}

Delete a single annotation by ID.

Path variables

id
integer required

Annotation ID.

Request parameters

force
integer optional

Bypass trash and force deletion?

Enumeration:
0

Trash (default).

1

Permanently delete.

Default:
0
Query Annotations
GET /wp-json/wp/v2/annotations

Also supports all the same arguments as the REST API for comments; e.g., pagination, order.

Request parameters

context
string optional

REST API context.

Enumeration:
view
edit
embed
Default:
view
post
string required

Post ID. Array or comma-delimited string.

Example:
1,2,3
parent
string optional

Parent annotation ID. Array or comma-delimited string. If hierarchical is given, parent defaults to 0 unless defined explicitly. This way hiearchical queries start from top-level annotations by default.

Example:
4,5,6
author
string optional

Author ID. Requires ability to edit_others_annotations. However, any authenticated user can set this to their own user ID, which allows them to query their own annotations.

status
string optional

Status. Array or comma-delimited string. To query a restricted status (hold, 0, spam, trash) you must be able to edit_others_annotations. However, if author is given, and the authenticated user is that author, and they can delete_annotations, trash becomes available.

Default:
approve
Example:
approve,resolve,reject,archive
via
string optional

Annotation client identifier. Array or comma-delimited string.

Example:
gutenberg,other
hierarchical
string optional

Hierarchical response format?

Enumeration:
flat

All children (deeply) are simply appended.

threaded

Each annotation includes a children property containing an array of any replies.

Example 1
Example 2

?context=view&hierarchical=threaded

[
    {
        "id": 1,
        "post": 1,
        "parent": 0,
        "author": 1,
        "author_name": "John",
        "author_url": "https://john.example.com",
        "date": "2018-01-11T12:21:10",
        "date_gmt": "2018-01-11T12:21:10",
        "content": {
            "rendered": "<p>Hello world!</p>\n"
        },
        "link": "https://example.com/hello-world/#comment-1",
        "status": "approved",
        "type": "annotation",
        "author_avatar_urls": {
            "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
            "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
            "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
        },
        "meta": [],
        "via": "gutenberg",
        "selector": {
            "type": "CssSelector",
            "value": "#foo"
        },
        "_links": {
            "self": [
                {
                    "href": "https://example.com/wp-json/wp/v2/annotations/1"
                }
            ],
            "collection": [
                {
                    "href": "https://example.com/wp-json/wp/v2/annotations"
                }
            ],
            "author": [
                {
                    "embeddable": true,
                    "href": "https://example.com/wp-json/wp/v2/users/1"
                }
            ],
            "up": [
                {
                    "embeddable": true,
                    "post_type": "post",
                    "href": "https://example.com/wp-json/wp/v2/posts/1"
                }
            ]
        },
        "children": [
            {
                "id": 2,
                "post": 1,
                "parent": 1,
                "author": 2,
                "author_name": "Mary",
                "author_url": "https://mary.example.com",
                "date": "2018-01-11T12:21:10",
                "date_gmt": "2018-01-11T12:21:10",
                "content": {
                    "rendered": "<p>Hello world!</p>\n"
                },
                "link": "https://example.com/hello-world/#comment-2",
                "status": "approved",
                "type": "annotation",
                "author_avatar_urls": {
                    "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
                    "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
                    "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
                },
                "meta": [],
                "via": "gutenberg",
                "selector": null,
                "_links": {
                    "self": [
                        {
                            "href": "https://example.com/wp-json/wp/v2/annotations/2"
                        }
                    ],
                    "collection": [
                        {
                            "href": "https://example.com/wp-json/wp/v2/annotations"
                        }
                    ],
                    "author": [
                        {
                            "embeddable": true,
                            "href": "https://example.com/wp-json/wp/v2/users/2"
                        }
                    ],
                    "up": [
                        {
                            "embeddable": true,
                            "post_type": "post",
                            "href": "https://example.com/wp-json/wp/v2/posts/1"
                        }
                    ]
                },
                "children": []
            }
        ]
    }
]

?context=edit&hierarchical=flat

[
    {
        "id": 1,
        "post": 1,
        "parent": 0,
        "author": 1,
        "author_name": "John",
        "author_email": "john@example.com",
        "author_url": "https://john.example.com",
        "author_ip": "192.168.42.1",
        "author_user_agent": "Mozilla/5.0",
        "date": "2018-01-11T12:21:10",
        "date_gmt": "2018-01-11T12:21:10",
        "content": {
            "rendered": "<p>Hello world!</p>\n",
            "raw": "Hello world!"
        },
        "link": "https://wp.vm/hello-world/#comment-1",
        "status": "approved",
        "type": "annotation",
        "author_avatar_urls": {
            "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
            "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
            "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
        },
        "meta": [],
        "via": "gutenberg",
        "selector": {
            "type": "CssSelector",
            "value": "#foo"
        },
        "_links": {
            "self": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations/1"
                }
            ],
            "collection": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations"
                }
            ],
            "author": [
                {
                    "embeddable": true,
                    "href": "https://wp.vm/wp-json/wp/v2/users/1"
                }
            ],
            "up": [
                {
                    "embeddable": true,
                    "post_type": "post",
                    "href": "https://wp.vm/wp-json/wp/v2/posts/1"
                }
            ]
        }
    },
    {
        "id": 2,
        "post": 1,
        "parent": 1,
        "author": 2,
        "author_name": "Mary",
        "author_email": "mary@example.com",
        "author_url": "https://mary.example.com",
        "author_ip": "192.168.42.1",
        "author_user_agent": "Mozilla/5.0",
        "date": "2018-01-11T12:21:10",
        "date_gmt": "2018-01-11T12:21:10",
        "content": {
            "rendered": "<p>Hello world!</p>\n",
            "raw": "Hello world!"
        },
        "link": "https://wp.vm/hello-world/#comment-2",
        "status": "approved",
        "type": "annotation",
        "author_avatar_urls": {
            "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
            "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
            "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
        },
        "meta": [],
        "via": "gutenberg",
        "selector": null,
        "_links": {
            "self": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations/2"
                }
            ],
            "collection": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations"
                }
            ],
            "author": [
                {
                    "embeddable": true,
                    "href": "https://wp.vm/wp-json/wp/v2/users/2"
                }
            ],
            "up": [
                {
                    "embeddable": true,
                    "post_type": "post",
                    "href": "https://wp.vm/wp-json/wp/v2/posts/1"
                }
            ]
        }
    }
]
Capabilities

Meta-caps check specific annotations by ID.

current_user_can( 'edit_annotation', 53254 )
  • read_annotation
  • edit_annotation
  • delete_annotation

This custom meta-cap checks a post by ID.

current_user_can( 'create_annotation', 123 )
  • create_annotation

General pseudo-caps that optionally support a post ID arg.

current_user_can( 'read_annotations', 123 )
current_user_can( 'read_annotations' )
  • read_annotations

Other general pseudo-caps.

current_user_can( 'edit_others_annotations' )
  • create_annotations
  • delete_annotations
  • delete_others_annotations
  • delete_private_annotations
  • delete_published_annotations
  • edit_annotations
  • edit_others_annotations
  • edit_private_annotations
  • edit_published_annotations
  • publish_annotations
  • read_private_annotations