The PMP API
You may want to go read the user guides before diving into this developer documentation.
The Public Media Platform is a Hypermedia API that is accessible on the web over HTTPS and allows API clients to communicate with the server using the Collection.doc+JSON media type. Collection.doc+JSON
is a recursive, generic, JSON hypermedia type optimized for the flexible exchange of structured content by content providers and consumers.
Don’t see what you’re looking for? You can also check our legacy docs.
These documents are hosted on Github, so consider contributing back to them!
Environments
Currently, PMP developers can register accounts in two environments:
Environment | Usage |
---|---|
https://api.pmp.io | Production data only - please do not perform any testing here. |
https://api-sandbox.pmp.io | Integration testing and development - appropriate environment for testing. |
Additionally, each environment has a read and write endpoint. For GET
requests, api.pmp.io or api-sandbox.pmp.io should be used. For POST/PUT/DELETE
requests, publish.pmp.io or publish-sandbox.pmp.io should be used. Since this is a hypermedia API, the documents themselves will tell you the endpoints to use. SDK’s should not hardcode any URL except for the API home document - follow the links!
The code examples in these docs will use api.pmp.io and publish.pmp.io - just replace with api-sandbox.pmp.io and publish-sandbox.pmp.io to use the testing environment.
SDKs
API clients are available in multiple languages. Whenever possible, we encourage you to use and contribute to an existing SDK project rather than roll your own. Contributions welcome!
Language | Links |
---|---|
Javascript | pmp-js-sdk on github / npm |
Perl | Net::PMP on github / cpan — Net::PMP::Profile on github / cpan |
PHP | phpsdk on github |
Python | pmp-myriad on github |
Python | py3-pmp-wrapper on github |
Ruby | pmp on github / rubygems |
If you know of another SDK not mentioned here, please let us know!!
Authentication
API users are given a username
and password
. These cannot be used directly to authenticate with the PMP - rather they are used to manage multiple API client credentials under a single user account. The PMP uses an OAuth2 Client Credentials flow, meaning that you will be generating a separate client_id
and client_secret
for each application that will use the API.
Let me just stress that you should NEVER be using your PMP username/password directly in an application. You should give each app its own client-id/secret, to use the PMP on your behalf.
Credential List
See urn:collectiondoc:form:listcredentials
in the home doc.
curl -u "username:password" -X GET "https://api.pmp.io/auth/credentials"
# returns a json string
# {
# "clients": [
# {
# "client_id": "405a022e-6274-4170-8eba-67933551c3c3",
# "client_secret": "22c2dbdc03d72e18cb01dee1",
# "scope": "write",
# "label": "hello world",
# "token_expires_in": 1209600
# }
# ]
# }
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({username: 'u', password: 'p', host: 'https://api.pmp.io'});
sdk.credList(function(resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(resp.radix); // [{...client object...}, {...}]
});
# currently no way to do this - use support.pmp.io instead
<?php
$user = new \Pmp\Sdk\AuthUser('https://api.pmp.io', 'myuser', 'mypassword');
$clients = $user->listCredentials()->clients();
echo count($clients);
echo $clients[0]->client_id;
echo $clients[0]->label;
?>
auth = PMP::Client.new(user: 'u', password: 'p', endpoint: 'https://api.pmp.io')
creds = auth.credentials.list
puts creds.clients.count
puts creds.clients.first.client_id
puts creds.clients.first.label
Credential Creation
See urn:collectiondoc:form:createcredentials
in the home doc.
- Use the “publish” endpoint
- Include the header
Content-Type: application/x-www-form-encoded
- May include the following optional form parameters:
Parameter | Default | Usage |
---|---|---|
scope | write | “read” or “write”, indicating if this client can write to the API |
label | (blank) | human readable label for this client |
token_exires_in | 1209600 | number of seconds a token generated by this client will last, until you must request it again. |
curl -u "username:password" -X POST -H "Content-Type: application/x-www-form-urlencoded" "https://publish.pmp.io/auth/credentials" -d "scope=read" -d "token_expires_in=999999" -d "label=somethingCool"
# returns a json string
# {
# "client_id": "405a022e-6274-4170-8eba-67933551c3c3",
# "client_secret": "22c2dbdc03d72e18cb01dee1",
# "token_expires_in": 999999,
# "scope": "read",
# "label": "somethingCool"
# }
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({username: 'u', password: 'p', host: 'https://api.pmp.io'});
sdk.credCreate('somethingCool', 'read', 999999, function(resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(resp.radix); // {...client object...}
});
my $auth = Net::PMP::Client->new(host => 'https://api.pmp.io', id => 0, secret => 0);
my $cred = $auth->create_credentials(
username => 'myname',
password => 'mypass',
scope => 'read',
expires => 100,
label => 'perltesting',
);
print $cred->client_id . "\n";
print $cred->client_secret . "\n";
<?php
$user = new \Pmp\Sdk\AuthUser('https://api.pmp.io', 'myuser', 'mypassword');
$cred = $user->createCredential('read', 999, 'testlabel');
echo $cred->client_id;
echo $cred->token_expires_in;
echo $cred->label;
?>
auth = PMP::Client.new(user: 'u', password: 'p', endpoint: 'https://api.pmp.io')
cred = auth.credentials.create(scope: 'read', label: 'test', token_expires_in: 100)
puts cred.client_id
puts cred.label
Credential Deletion
See urn:collectiondoc:form:removecredentials
in the home doc.
- Use the “publish” endpoint
- Include the
client_id
in the URL
curl -u "username:password" -X DELETE "https://publish.pmp.io/auth/credentials/405a022e-6274-4170-8eba-67933551c3c3"
# returns 204 - no content
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({username: 'u', password: 'p', host: 'https://api.pmp.io'});
sdk.credDestroy('405a022e-6274-4170-8eba-67933551c3c3', function(resp) {
console.log(resp.status); // 204
console.log(resp.success); // true
});
my $auth = Net::PMP::Client->new(host => 'https://api.pmp.io', id => 0, secret => 0);
$auth->delete_credentials(
username => 'myname',
password => 'mypass',
client_id => $cred->client_id,
);
<?php
$user = new \Pmp\Sdk\AuthUser('https://api.pmp.io', 'myuser', 'mypassword');
$user->removeCredential('405a022e-6274-4170-8eba-67933551c3c3');
?>
auth = PMP::Client.new(user: 'u', password: 'p', endpoint: 'https://api.pmp.io')
auth.credentials.destroy('405a022e-6274-4170-8eba-67933551c3c3')
Token Creation
To make authenticated requests to the API, you need a valid bearer token. This is normally handled transparently by the client SDK, but if you’d like to get one manually:
See urn:collectiondoc:form:issuetoken
in the home doc.
- Use the main request endpoint [api.pmp.io](https://api.pmp.ioDo rather than “publish”, even though this is a POST request
- Set the Content-Type header to
application/x-www-form-encoded
- Include your
client_id
andclient_secret
for the authorization flow - Include the form parameter
grant_type=client_credentials
curl -u "client_id:client_secret" -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials" "https://api.pmp.io/auth/access_token"
# returns a json string
# {
# "access_token": "3f2401ae1a74adf8b14a638a",
# "token_type": "Bearer",
# "token_issue_date": "2014-08-25T20:05:19+00:00",
# "token_expires_in": 999634
# }
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({client_id: '1', client_secret: '2', host: 'https://api.pmp.io'});
sdk.token(function(token) {
console.log(token); // "3f2401ae1a74adf8b14a638a"
// or null (if unauthorized, bad host, etc)
});
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
my $token = $client->get_token();
<?php
$auth = new \Pmp\Sdk\AuthClient('https://api.pmp.io', 'myid', 'mysecret');
$token = $auth->getToken();
echo $token->access_token;
echo $token->token_issue_date;
?>
pmp = PMP::Client.new(client_id: '1', client_secret: '2', endpoint: 'https://api.pmp.io')
oauth_token = pmp.token
# raises Faraday::ConnectionFailed for bad host
# raises OAuth2::Error if unauthorized
puts oauth_token.token # the string
In the response, token_expires_in
is the number in seconds until this access_token
will expire and your client will need to request a new one. Requesting a non-expired access token for the same client will return the same access_token
with a renewed expiration date. SDK’s should automatically request to renew the access token when it expires.
Token Deletion
See urn:collectiondoc:form:revoketoken
in the home doc.
- Use the “publish” endpoint
- Make an HTTP
DELETE
request
curl -u "client_id:client" -X DELETE "https://api.pmp.io/auth/access_token"
# returns 204 - no content
// the javascript sdk doesn't provide an explicit way to do this
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
$client->revoke_token();
<?php
$auth = new \Pmp\Sdk\AuthClient('https://api.pmp.io', 'myid', 'mysecret');
$auth->revokeToken();
?>
# the ruby sdk doesn't provide an explicit way to do this
Collection.doc+JSON
Aside from authentication, PMP API responses are exclusively of the media type Collection.doc+JSON
. For a real example, check out the PMP home document. But it is basically a JSON object with the following top-level structure:
{
"version": "1.0",
"href": "https://api.pmp.io/docs/04224975-e93c-4b17-9df9-96db37d318f3",
"attributes": {"keys": "values"},
"links": {"keys": "values"},
"errors": {"keys": "values"}
}
Key | Usage |
---|---|
version |
The cdoc specification version this resource adheres to. |
href |
A valid URI identifier for this resource… aka the document permalink. |
attributes |
A set of properties describing the state of this resource. |
links |
Hyperlinks to the actions and relationships for this resource. |
items |
A convenience link to cdocs referenced by links.item . |
errors |
Additional information about API errors. |
Headers
Content-Type
The primary MIME type used by the PMP is application/vnd.collection.doc+json
. You can expect to see this in the Content-Type
header in responses from the PMP and you should set this as the Content-Type
when you are interacting with the PMP.
Response Sizes
PMP responses include all sorts of boilerplate links. Clients really only need to get this information once, and can afterwards ignore the information. To give clients some options for what data they would like in the response, the PMP utilizes the HTTP Prefer header. To assist in caching, variable preferences will also be included in the Vary
response header.
By default (not specifying a Prefer
header), you’re going to see a response header of Vary: Prefer, Accept-Encoding
and you will get the full set of PMP boilerplate links.
If you want a minimal response, set Prefer: return=minimal
. The PMP will include the usual Vary: Prefer, Accept-Encoding
, plus an additional header to indicate that it did apply your preference: Preference-Applied: return=minimal
. This response will NOT include any PMP boilerplate links.
In addition to preferences, you can also get gzipped responses from the PMP by setting Accept-Encoding: gzip, deflate
. However, the response will only actually be gzipped if it is large enough that the PMP deems it worthwhile.
Caching
PMP responses will all set the usual ETag
and Last-Modified
headers.
To check for 304-Not-Modified on the client side, send one of these standard HTTP headers:
Also be sure to check the Vary
header - which indicates ways that your response for a resource may be different than another request for the same resource.
Core Attributes
{
"attributes": {
"guid": "04224975-e93c-4b17-9df9-96db37d318f4",
"title": "A PMP Collection Document",
"created": "2014-07-04T04:00:44+00:00",
"modified": "2014-09-08T20:56:59+00:00",
"valid": {
"from": "2013-07-04T04:00:44+00:00",
"to": "3013-07-04T04:00:44+00:00"
},
"hreflang": "en"
}
}
Document attributes take the form of a {"attr_key_123": "value"}
hash, where the value may be a string, array, or object.
The following attributes are defined by the Core Schema, and common to all PMP documents:
Key | Usage |
---|---|
guid |
A UUIDv4 assigned to this document. readonly |
title |
A human-meaningful title for this document. |
created |
Document creation timestamp in ISO 8601 readonly |
modified |
Document last modified timestamp in ISO 8601 readonly |
valid |
Object {from: "", to: ""} with two ISO 8601 timestamps between which the document is valid. Defaults from the doc created timestamp + 1000 years. |
hreflang |
Language of this document using ISO639-1 code. Defaults to “en”. |
Core Links
{
"links": {
"profile": [
{
"href": "http://api.pmp.io/profiles/core"
}
],
"alternate": [
{
"href": "http://support.pmp.io/docs#collection-docjson-core-links",
"title": "Core links html docs"
},
{
"href": "http://cdoc.io/spec.html#links"
}
]
}
}
The links section of the document is a hash of {"link_relation_ship_type": ["array", "of", "link", "objects"]}
.
The relationship type is a descriptive string of how the current document is related to the linked document(s).
These relationship types are defined by the Core Schema and common to all PMP documents:
Key | Usage |
---|---|
alternate |
Links that represent alternate representations of this document. |
auth |
Links describing how to authorize against the PMP. readonly |
collection |
Links that represent the collection documents that contain this document. Typically used as a way to model an unordered parent/child relationship; the collection link indicates that the current resource has a loose coupling to another resource document that can be viewed as a collection of other documents. A resource may be linked to multiple collections. Order can not be communicated via collection links; if order of documents in a collection is important, use item links within the collection document itself. |
creator |
Auto-generated link representing the user that created this document. readonly |
distributor |
Links that represent users that can legally distribute this document. |
edit |
Links that represent URLs for updates to this document. readonly |
item |
Links that represent documents that are items of this document. Typically used as a way to model an ordered parent/child relationship, these item documents are often associated assets such as images or audio files that are tightly coupled to this linking document. A resource may be linked as an item of multiple documents. Order is meaningful; item links are a way for a document to communicate its ordered structure. |
navigation |
Links that represent navigation and pagination for this document. readonly |
owner |
Links that represents users that legally own this document. Defaults to creator . required |
permission |
Links that represent permission groups associated with this document. |
profile |
Link that represents the profile of this document. required |
query |
Links that represent templated queries that can run against this document. readonly |
Link Objects
The link objects contained in the relationship-type array must have, at a minimum, either an href
or an href-template
.
If an href-template
is used, then an href-vars
object must also be included to describe the variables in that template.
This is the complete structure of a “link” object, as defined by the Core Schema.
All keys are optional except for those described previously.
Key | Usage |
---|---|
hints |
Object containing hints about interacting with the link, such as HTTP methods. |
href |
URL of the linked document. required |
href-template |
A RFC6570 templated URL. required |
href-vars |
Object of the format {"varname": "http://descriptive/link"} , linking href-template variables to their documentation. required |
hreflang |
Language of this document using ISO639-1 code. |
method |
Expected HTTP method to use for the link |
pagenum |
The page number represented by the link |
rels |
Array of the format ["urn:pmp:something", "urn:collectiondoc:something"] describing additional relationship types on this link |
title |
The title of the linked document |
totalitems |
The total number of items found in the full document represented by the link |
totalpages |
The total number of pages for the full document represented by the link |
type |
Mimetype of the linked document |
Profiles and Schemas
While Collection.doc+JSON
is flexible enough to accommodate most new content types, it is too loosely defined to be sufficient in and of itself. Any specific application will need something that allows tailoring of Collection.doc+JSON
with additional semantics. In the Hypermedia world, the standard that allows such tailoring is the Profile link relation type.
Profile documents
For any PMP document, the link.profile[0].href
must point to a dereferenceable profile=profile document. So for a profile=story document, this would be something like [{href: "https://api.pmp.io/profiles/story"}]
. Now, if we go fetch that profile document, we’ll get something like this:
{
"version": "1.0",
"href": "https://api.pmp.io/profiles/story",
"attributes": {
"title": "Story Profile"
},
"links": {
"profile": [{
"href": "https://api.pmp.io/profiles/profile"
}],
"extends": [{
"href": "https://api.pmp.io/profiles/base"
}],
"schema": [{
"href": "https://api.pmp.io/schemas/story",
"scope": "update"
}]
}
}
Notice those links on the profile doc:
links.profile
tells us that this story document is a profile.This document “type” is important for interpreting the attributes and links of any document. It also lets us group together similar types of documents, such as stories or audio or topics.
links.extends
gives us a hierarchy of profiles.Inheritance is cool! Now we can group together types of documents in a hierarchical way. All story documents are also base documents. But not all base documents are story documents. See the profile hierarchy tree for more.
links.schema
links to a dereferenceableprofile=schema
document.This optional link defines a schema to which all story documents must adhere. It must be a “profile=schema” document. The schema can require certain attributes and links be present in all “stories”, so we can interpret and display them in a consistent fashion.
For a more in-depth look at profiles, check out the legacy docs.
Schema documents
To enforce validation of different document types (profiles), the PMP uses schema documents. These are basically a JSON schema definition wrapped inside a Collection.doc+JSON
document. For instance, the “story” schema looks something like:
{
"version": "1.0",
"href": "https://api.pmp.io/schemas/story",
"attributes": {
"title": "Story Schema",
"schema": { "a json schema": "goes here" }
},
"links": {
"profile": [{
"href": "https://api.pmp.io/profiles/schema"
}]
}
}
The important things to notice are that:
links.profile
tells us that this is a schema.attributes.schema
is a JSON schema object.
For more info on interpreting JSON schemas, check out the json-schema.org docs, which provides simple and advanced examples.
Hierarchy
The basic PMP profiles can be visually represented through a tree graph:
Here you can see that an audio document is also a media and base document. Since our JSON schema for base requires attributes.title
to be present, that requirement also cascades down to audio.
And when we search the PMP for media documents, we know that we will also be getting audio, images and videos.
This is by no means an exhaustive list of profiles in the PMP. (You can also create your own - just go search for profiles to see them all). As a best practice, any user-defined profile should extend from these system-defined profiles.
Dictionary
To help interpret PMP documents, here is a list of some of the common PMP profiles and how they are used. Keep in mind that all these attributes/links are in addition to those defined by the Collection.doc+JSON standard.
Story
The Story profile is the glue of the PMP!
- Primary document type to which most media assets (images/audio/video) are attached
- Provides context for
links.item
assets - Organizes itself into collections (topics/series/properties)
- Identifies authors and contributors to the story
- Tags can be used to get all related Stories for a certain collaboration or event
It is not required to use Story documents, but it is highly suggested to use a Story or a sub-type of a Story when creating a complex document (e.g. one with child assets, such as media docs).
Story Attributes
Key | Value |
---|---|
title |
A human-meaningful title for this story required |
byline |
Rendered byline as suggested by document distributor |
description |
Content of this document without HTML co-required |
contentencoded |
Content which can be used literally as HTML-encoded content co-required |
contenttemplated |
Content which has placeholders for rich-media assets co-required |
teaser |
A short description, appropriate for showing in small spaces |
tags[] |
An array of human-meaningful tags for this story |
Story Links
relType | Key | Value |
---|---|---|
item |
href |
Media documents (audio/image/video) associated with this story |
rels[] |
Array of relationship types for this item | |
collection |
href |
Collections (often properties, series, topics) this story is a member of |
rels[] |
Array of relationship types for this collection | |
author |
href |
Docs (often contributors, users, organizations) that represent the authors of this story |
alternate |
href |
The url for a web page showing this story on the publisher or producer’s site |
type |
Should be text/html (since link is not a doc) |
|
copyright |
href |
The url for a web page showing the copyright terms for this story |
type |
Should be text/html (since link is not a doc) |
|
title |
Text to be displayed for the copyright link |
Episode
An Episode is a sub-type of a Story that indicates the document is the full episode of a series, in the form that it might have been produced for broadcast. An Episode will often contain multiple segments that could also be considered stories themselves.
Episode Links
relType | Key | Value |
---|---|---|
item |
href |
Ordered set of Story documents that make up this episode |
rels[] |
Array of relationship types for this item |
Image
The Image profile is used to represent a distinct image with multiple crops. Each crop (a binary file) is represented by an enclosure
link, which can be identified either by its meta.crop
or meta.height
and meta.width
.
Image Attributes
Key | Value |
---|---|
title |
Alt: human-meaningful alternate text for the image required |
byline |
Credit: a rendered byline for the image |
description |
Caption: additional text that explains and/or complements the image |
Image Links
relType | Key | Value |
---|---|---|
enclosure |
href |
URL for the binary image file required |
type |
The MIME-type of the image file required | |
meta.crop |
The semantic crop identifier for this file | |
meta.height |
Height in pixels | |
meta.width |
Width in pixels | |
meta.resolution |
Resolution in pixels per inch (ppi) | |
copyright |
href |
URL for a web page showing the copyright terms for this image |
type |
Should be text/html (since link is not a doc) |
|
title |
Text to be displayed for the copyright link |
Audio
The Audio profile is used to represent multiple sources/qualities/bitrates of a piece of audio. These are each encapsulated in their own enclosure
link that clients can iterate through to find the one they would like to use.
Audio Attributes
Key | Value |
---|---|
title |
Alt: human-meaningful alternate text for the audio required |
byline |
Credit: a rendered byline for the audio |
description |
Caption: additional text that explains and/or complements the audio |
contentencoded |
HTML representation of the binary file, with the audio encoded into the body co-required |
Audio Links
relType | Key | Value |
---|---|---|
enclosure |
href |
URL for the binary audio file co-required |
type |
The MIME-type of the audio file required | |
meta.codec |
Audio codec | |
meta.format |
Audio format | |
meta.duration |
Audio duration in seconds | |
oembed |
href |
URL for a valid oembed object representing this audio co-required |
copyright |
href |
URL for a web page showing the copyright terms for this audio |
type |
Should be text/html (since link is not a doc) |
|
title |
Text to be displayed for the copyright link |
Video
The Video profile represents multiple formats/sources of a video, each of which is given its own enclosure
link.
Unlike Images or Audio, it is common for a Video to lack any public link to the original binary file. This is addressed via Embeddable Media, a practice which is still evolving.
Video Attributes
Key | Value |
---|---|
title |
Alt: human-meaningful alternate text for the video required |
byline |
Credit: a rendered byline for the video |
description |
Caption: additional text that explains and/or complements the video |
contentencoded |
HTML representation of the binary file, with the video encoded into the body co-required |
Video Links
relType | Key | Value |
---|---|---|
enclosure |
href |
URL for the binary video file co-required |
type |
The MIME-type of the video file required | |
meta.codec |
Video codec | |
meta.format |
Video format | |
meta.duration |
Video duration in seconds | |
oembed |
href |
URL for a valid oembed object representing this video co-required |
copyright |
href |
URL for a web page showing the copyright terms for this video |
type |
Should be text/html (since link is not a doc) |
|
title |
Text to be displayed for the copyright link |
Best Practices
Publishers to the PMP are encouraged to follow best practices for how they structure and persist their content.
If a publisher uses idiosyncratic fields, attributes, or profiles, the documents may be very useful to them, but will be less useful (or even discoverable) to consumers attempting to use content from across the PMP. Below are a set of documented best practices to encourage publishers to post content to the PMP in a standard, useful way that will be consistent with the majority of PMP published content.
Standard Profiles
The PMP is flexible. It allows a publisher to create their own content profiles and schemas, so that almost any kind of content document could be published. However, clients of the PMP will find it difficult to discover, display or otherwise use content that does not use or extend the basic content types.
Before creating a new Profile, examine the existing ones in common use, and see if they can be used. If not, refer to the Base Content Profile, and try to extend it for your use case.
Using or extending a standard profile will also help with discoverability via search. When searching by document profile, the PMP returns not only the documents for that exact profile, but also documents where the profile is extended from that profile.
Use the media types for images, audio and video.
Use a story document to pull together multiple documents and as a default lead document type.
GUIDs
The PMP API will not save a document without a pre-defined GUID (globally unique document identifier). Each publisher is responsible for generating a properly formatted GUID for a document before saving it to the PMP.
For globally unique document identifiers PMP uses UUID version 4 identifiers based on RFC 4122, represented as 32 hexadecimal digits with optional dashes after the 8th, 12th, 16th, and 20th digits.
The PMP accepts GUIDs with or without dashes; it normalizes them before validation.
Bylines
Bylines for documents should be rendered as follows:
Single author
Firstname [Middlename/Initial] Lastname
Two authors (separated by “and”)
Firstname Lastname and Someone T Else
Three or more authors (commas and “and” - no Oxford comma)
Firstname Mid Lastname, Person One, Someone Else and Finally T Last
Do NOT add a prefix (such as “By”) to the byline field. The logic being that it is far easier to prepend a prefix, rather than remove one.
Tags and Itags
PMP documents may have two separate sets of free-form tag attributes.
Name | Description |
---|---|
attributes.tags[] |
An array of human-readable tags or keywords associated with the document |
attributes.itags[] |
An array of internal tags for use by the publisher - often used for external identifiers, publisher-specific labeling systems, etc. |
Consider carefully whether or not the tag you are creating is for internal reference (itags
) or public discoverability (tags
).
Story Content
When creating Story documents, set at least one “content” field. This will be searched by default when using the ?text=foobar
query parameter.
Name | Description |
---|---|
description |
Text only version |
contentencoded |
HTML version, with media assets optionally encoded into the body. |
contenttemplated |
Structured to have related asset docs embedded into the template (format TBD). |
Item Links
When a document “contains” other documents, you should use links.item
in the parent doc to claim (and optionally order) the children.
The most common use case for this is in Story docs, when they “contain” associated image/audio/video media. In this case, you should also set a rels
key on each links.item
, to indicate to consumers what type of child item each is. This example illustrates common/known rels for Story items:
itemLink.href = "https://api.pmp.io/docs/<MEDIA_GUID>";
itemLink.rels = ["urn:collectiondoc:image"]; // if href has profile=image
itemLink.rels = ["urn:collectiondoc:audio"]; // ... or audio
itemLink.rels = ["urn:collectiondoc:video"]; // ... or video
// or maybe this is an episode with multiple story-items
itemLink.href = "https://api.pmp.io/docs/<STORY_1_GUID>";
itemLink.rels = ["urn:collectiondoc:story"];
Collection Links
When a document “belongs to” a collection of docs (often described as a parent/child documents), use links.collection
to indicate that relationship. Note that you cannot indicate order using links.collection
. If you need to enforce order, use links.item
instead.
This occurs most commonly in Story documents claiming to be part of a Property, Series, Contributor or Topic. You can set one of the following rels
on your collection links, to help consumers differentiate between them:
collectionLink.href = "https://api.pmp.io/docs/<COLLECTION_GUID>";
collectionLink.rels = ["urn:collectiondoc:collection:property"]; // if href has profile=property
collectionLink.rels = ["urn:collectiondoc:collection:series"]; // ... or series
collectionLink.rels = ["urn:collectiondoc:collection:contributor"]; // ... or contributor
collectionLink.rels = ["urn:collectiondoc:collection:topic"]; // ... or topic
Topic collections
To ensure consistency among PMP publishers, map your documents to one of the standard PMP Topics:
Topic | Href | Guid |
---|---|---|
Arts | https://api.pmp.io/topics/arts | 89944632-fe7c-47df-bc2c-b2036d823f98 |
Culture | https://api.pmp.io/topics/culture | 9c3c8f9e-f038-4b7e-9719-f5c4b1404c1a |
Education | https://api.pmp.io/topics/education | 7a1c4403-0bce-4866-a9eb-8066da226985 |
Food | https://api.pmp.io/topics/food | 635bb7a1-9e49-4a5d-bab3-048ff945b5fb |
Health | https://api.pmp.io/topics/health | 9bd2ec35-be2f-4eb1-9ef9-54a59405dd85 |
Money | https://api.pmp.io/topics/money | 4d0acb4c-7057-4771-987d-97fc21ad0bcc |
Music | https://api.pmp.io/topics/music | 4993cf23-968a-4182-acb2-4d46a96d0ac8 |
News | https://api.pmp.io/topics/news | 5c0f1387-024e-4a84-8804-43048779cc37 |
Politics | https://api.pmp.io/topics/politics | 588eb430-d600-4617-ab8f-f601839436a9 |
Science | https://api.pmp.io/topics/science | b6ef8e81-9ad2-4e34-8fbd-22181fe1b0e0 |
Sports | https://api.pmp.io/topics/sports | 44ed7afc-0dd7-4aa1-8c88-34e74dc0d36b |
Technology | https://api.pmp.io/topics/technology | 3f829119-5310-43b9-acc5-0f36a51aae42 |
Image crops
Enclosures for images include the metadata field crop
. For consistency, refer to this set of known “semantic crop identifiers”:
Crop | Description |
---|---|
primary |
The enclosure that should be used when displaying the full story |
large |
The largest available crop, suitable for a lightbox |
medium |
Mid-sized enclosure to use in a teaser panel |
small |
Smallest crop available for the image |
square |
Square crop of a primary-or-small width/height |
Other crops seen in the wild:
standard
wide
enlargement
custom
Embedded Media
Sometimes it is not possible to provide URLs to the actual media file.
In that case, the producer can provide HTML code to embed a player for the media in a web page, or a link to a page to access the media file (e.g. a YouTube URL).
A best practice for how to share embedded media has not yet been established, but please reference these current suggestions, which include the use of oembed type links, setting a new attribute on the video doc, or using the contentencoded
attribute for the HTML to embed.
Examples
Now for the main event: making authenticated requests against the API. This covers some common use cases around interacting with the API, but is by no means exhaustive. For more information on the format of the data returned by the server, see the Collection.doc+JSON section above.
Fetching documents
One of the basic PMP usages is fetching a single document. For instance, let’s fetch the “arts topic” document, with a GUID of 89944632-fe7c-47df-bc2c-b2036d823f98
…
curl -H "Authorization: Bearer 3f2401ae1a74adf8b14a638a" -X GET "https://api.pmp.io/docs/89944632-fe7c-47df-bc2c-b2036d823f98"
# returns a big collection+doc json string
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({client_id: '1', client_secret: '2', host: 'https://api.pmp.io'});
sdk.fetchDoc('89944632-fe7c-47df-bc2c-b2036d823f98', (doc, resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(doc.attributes.guid); // "89944632-fe7c-47df-bc2c-b2036d823f98"
console.log(doc.attributes.title); // "Arts Topic"
});
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
my $doc = $client->get_doc_by_guid('89944632-fe7c-47df-bc2c-b2036d823f98');
print $doc->get_guid . "\n"; # "89944632-fe7c-47df-bc2c-b2036d823f98"
print $doc->get_title . "\n"; # "Arts Topic"
<?php
$sdk = new \Pmp\Sdk('https://api.pmp.io', 'myid', 'mysecret');
$doc = $sdk->fetchDoc('89944632-fe7c-47df-bc2c-b2036d823f98');
if ($doc) {
echo "{$doc->attributes->guid}\n"; // "89944632-fe7c-47df-bc2c-b2036d823f98"
echo "{$doc->attributes->title}\n"; // "Arts Topic"
}
else {
echo "failed to fetch the ARTS topic - must have been a 403 or 404.\n";
}
?>
pmp = PMP::Client.new(client_id: '1', client_secret: '2', endpoint: 'https://api.pmp.io')
doc = pmp.query['urn:collectiondoc:hreftpl:docs'].where(guid: '89944632-fe7c-47df-bc2c-b2036d823f98')
puts doc.guid # "04224975-e93c-4b17-9df9-96db37d318f3"
puts doc.title # "Arts Topic"
Alternatively, we can fetch some documents by their “alias”, rather than the GUID. For the “arts topic” document, we can fetch it via the arts alias, using the topics endpoint (see urn:collectiondoc:hreftpl:topics
in the home doc).
curl -H "Authorization: Bearer 3f2401ae1a74adf8b14a638a" -X GET "https://api.pmp.io/topics/arts"
# returns a big collection+doc json string
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({client_id: '1', client_secret: '2', host: 'https://api.pmp.io'});
sdk.fetchTopic('arts', (doc, resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(doc.attributes.guid); // "89944632-fe7c-47df-bc2c-b2036d823f98"
console.log(doc.attributes.title); // "Arts Topic"
});
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
my $root = $client->get_doc();
my $uri = $root->query('urn:collectiondoc:hreftpl:topics')->as_uri( { guid => 'arts' } );
my $doc = $client->get_doc($uri);
print $doc->get_guid . "\n"; # "89944632-fe7c-47df-bc2c-b2036d823f98"
print $doc->get_title . "\n"; # "Arts Topic"
<?php
$sdk = new \Pmp\Sdk('https://api.pmp.io', 'myid', 'mysecret');
$doc = $sdk->fetchTopic('arts');
if ($doc) {
echo "{$doc->attributes->guid}\n"; // "89944632-fe7c-47df-bc2c-b2036d823f98"
echo "{$doc->attributes->title}\n"; // "Arts Topic"
}
else {
echo "failed to fetch the ARTS topic - must have been a 403 or 404.\n";
}
?>
pmp = PMP::Client.new(client_id: '1', client_secret: '2', endpoint: 'https://api.pmp.io')
doc = pmp.query['urn:collectiondoc:hreftpl:topics'].where(guid: 'arts')
puts doc.guid # "04224975-e93c-4b17-9df9-96db37d318f3"
puts doc.title # "Arts Topic"
Fetching collections
Next, let’s say that we want to find all stories belonging to the “arts” topic. That is, all stories with a links.collection
pointing to the “arts” topic.
If we know the GUID of the collection, we can do a query-by-guid:
curl -H "Authorization: Bearer 3f2401ae1a74adf8b14a638a" -X GET "https://api.pmp.io/docs?collection=89944632-fe7c-47df-bc2c-b2036d823f98"
# returns a big collection+doc json string
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({client_id: '1', client_secret: '2', host: 'https://api.pmp.io'});
sdk.queryDocs({collection: '89944632-fe7c-47df-bc2c-b2036d823f98', profile: 'story'}, (query, resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(query.items.length); // 10
console.log(query.total()); // 999
console.log(query.items[0].attributes.title); // "Some doc title"
});
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
my $search = $client->search({collection => '89944632-fe7c-47df-bc2c-b2036d823f98', profile => 'story'});
my $results = $search->get_items();
printf( "total: %s\n", $results->total );
while ( my $r = $results->next ) {
printf( '%s: %s [%s]', $results->count, $r->get_uri, $r->get_title, ) );
}
<?php
$sdk = new \Pmp\Sdk('https://api.pmp.io', 'myid', 'mysecret');
$doc = $sdk->queryCollection('89944632-fe7c-47df-bc2c-b2036d823f98', array('profile' => 'story'));
if ($doc) {
$items = $doc->items();
$count = count($items);
$total = $items->total();
echo "COUNT=$count TOTAL=$total\n";
foreach ($items as $item) {
echo " {$item->attributes->title}\n";
}
}
else {
echo "got 0 search results back\n";
}
?>
pmp = PMP::Client.new(client_id: '1', client_secret: '2', endpoint: 'https://api.pmp.io')
search = pmp.query['urn:collectiondoc:query:docs'].where(collection: '89944632-fe7c-47df-bc2c-b2036d823f98', profile: 'story')
if search && search.navigation && search.navigation[:self]
puts "total = #{search.navigation[:self].totalitems}"
end
search.items.each do |item|
puts "item = #{item.title} -> #{item.published}"
end
We can also use the collections endpoint to query within the collection (see urn:collectiondoc:query:collection
in the home doc). We could use a guid
here, but let’s use the arts collection alias.
curl -H "Authorization: Bearer 3f2401ae1a74adf8b14a638a" -X GET "https://api.pmp.io/collection/arts?profile=story"
# returns a big collection+doc json string
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({client_id: '1', client_secret: '2', host: 'https://api.pmp.io'});
sdk.queryCollection('arts', {profile: 'story'}, (query, resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(query.items.length); // 10
console.log(query.total()); // 999
console.log(query.items[0].attributes.title); // "Some doc title"
});
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
my $root = $client->get_doc();
my $uri = $root->query('urn:collectiondoc:query:collection')->as_uri({guid => 'arts', profile => 'story'});
my $search = $client->get_doc($uri);
my $results = $search->get_items();
printf( "total: %s\n", $results->total );
while ( my $r = $results->next ) {
printf( '%s: %s [%s]', $results->count, $r->get_uri, $r->get_title, ) );
}
<?php
$sdk = new \Pmp\Sdk('https://api.pmp.io', 'myid', 'mysecret');
$doc = $sdk->queryCollection('arts', array('profile' => 'story'));
if ($doc) {
$items = $doc->items();
$count = count($items);
$total = $items->total();
echo "COUNT=$count TOTAL=$total\n";
foreach ($items as $item) {
echo " {$item->attributes->title}\n";
}
}
else {
echo "got 0 search results back\n";
}
?>
pmp = PMP::Client.new(client_id: '1', client_secret: '2', endpoint: 'https://api.pmp.io')
search = pmp.query['urn:collectiondoc:query:collection'].where(guid: 'arts', profile: 'story')
if search && search.navigation && search.navigation[:self]
puts "total = #{search.navigation[:self].totalitems}"
end
search.items.each do |item|
puts "item = #{item.title} -> #{item.published}"
end
Searching the PMP
For open-ended searches, the docs query is your go-to endpoint (see urn:collectiondoc:query:docs
in the home doc).
For a complete list of PMP search parameters, check out the legacy querying docs.
For now, let’s just try a text search…
curl -H "Authorization: Bearer 3f2401ae1a74adf8b14a638a" -X GET "https://api.pmp.io/docs?profile=story&text=penmanship"
# returns a big collection+doc json string
var PmpSdk = require('pmpsdk');
var sdk = new PmpSdk({client_id: '1', client_secret: '2', host: 'https://api.pmp.io'});
sdk.queryDocs({profile: 'story', text: 'penmanship'}, (doc, resp) {
console.log(resp.status); // 200
console.log(resp.success); // true
console.log(query.items.length); // 10
console.log(query.total()); // 999
console.log(query.items[0].attributes.title); // "Some doc title"
});
my $client = Net::PMP::Client->new(id => '1', secret => '2', host => 'https://api.pmp.io');
my $search = $client->search({profile => 'story', text => 'penmanship'});
my $results = $search->get_items();
printf( "total: %s\n", $results->total );
while ( my $r = $results->next ) {
printf( '%s: %s [%s]', $results->count, $r->get_uri, $r->get_title, ) );
}
<?php
$sdk = new \Pmp\Sdk('https://api.pmp.io', 'myid', 'mysecret');
$doc = $sdk->queryDocs(array('profile' => 'story', 'text' => 'penmanship'));
if ($doc) {
$items = $doc->items();
$count = count($items);
$total = $items->total();
echo "COUNT=$count TOTAL=$total\n";
foreach ($items as $item) {
echo " {$item->attributes->title}\n";
}
}
else {
echo "got 0 search results back\n";
}
?>
pmp = PMP::Client.new(client_id: '1', client_secret: '2', endpoint: 'https://api.pmp.io')
search = pmp.query['urn:collectiondoc:query:docs'].where(profile: 'story', text: 'penmanship')
if search && search.navigation && search.navigation[:self]
puts "total = #{search.navigation[:self].totalitems}"
end
search.items.each do |item|
puts "item = #{item.title} -> #{item.published}"
end