The PMP API

First time in the PMP docs?
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.

You should normally use the support site to manage credentials. This section is for reference, in case you want to manage them manually.

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 and client_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.
IMPORTANT: Although this structure is defined by the Collection.doc spec, the authoritative PMP implementation can always be found in the Core Schema. Read more about Schemas and Profiles further down.

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”.
{
  "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

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:

  1. 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.

  2. 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.

  3. links.schema links to a dereferenceable profile=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:

  1. links.profile tells us that this is a schema.
  2. 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

REMEMBER: as a convenience, any “links.item” set on a doc will automagically be loaded into the top-level `items` array when you fetch that document.
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:

  1. Single author

    Firstname [Middlename/Initial] Lastname

  2. Two authors (separated by “and”)

    Firstname Lastname and Someone T Else

  3. 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).

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"];

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