Skip to main content

Sharing and Publishing

Learn how to share assets and boards with external stakeholders and publish boards to the web.

Overview​

Playbook offers two main ways to make your content accessible to others:

  • Shared Links: Private, trackable links for specific assets or boards
  • Published Links: Public web pages showcasing your boards

Prerequisites​

  • Access Token: API token with sharing permissions
  • Organization Slug: Your organization identifier
  • Asset/Board Tokens: IDs of content you want to share

Sharing Assets​

Create temporary shared links for individual assets.

curl -X POST "https://api.playbook.com/v1/my-org/assets/product-photo/share" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": {
"url": "https://playbook.com/s/abc123/product-photo"
}
}

JavaScript Example​

async function shareAsset(assetToken) {
const response = await fetch(
`https://api.playbook.com/v1/${ORG_SLUG}/assets/${assetToken}/share`,
{ method: "POST", headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } },
);

const { data } = await response.json();
return data.url;
}

// Usage
const shareUrl = await shareAsset("product-photo");
console.log("Share link:", shareUrl);

Sharing Boards​

Create shared links for entire boards, allowing external viewers to see all assets within.

curl -X POST "https://api.playbook.com/v1/my-org/boards/main-collection/share" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": {
"url": "https://playbook.com/s/def456/main-collection"
}
}

Board Sharing Features​

Shared board links include:

  • All assets in the board
  • Nested subboards (if applicable)
  • Asset metadata (title, description, tags)
  • Comments (optionally)
  • Download capabilities (based on permissions)

Board share and publish requests accept the same optional controls:

curl -X POST "https://api.playbook.com/v1/my-org/boards/main-collection/share" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"expiration_date": "2026-09-01T12:00:00Z",
"disable_downloads": true,
"password": "client-review"
}'

Omitting a field preserves its existing value. Send an empty string for expiration_date or password to clear that restriction; send disable_downloads: false to allow downloads again. Sharing plans without link customization are forced to the 30-day expiry and cannot add download/password restrictions. Published-page controls are not individually plan-gated; the plan limits publishing itself.

Board share requests also accept enable_uploads. It lets anyone holding the link add files to the board without signing in — the way to collect files you cannot fetch yourself: give the URL to a client or a teammate and ask them to drop the files on it, then poll the board for what arrives.

curl -X POST "https://api.playbook.com/v1/my-org/boards/main-collection/share" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "enable_uploads": true, "password": "drop-files", "expiration_date": "2026-09-01T12:00:00Z" }'

Response:

{
"data": {
"url": "https://playbook.com/s/def456/main-collection",
"uploads_enabled": true
}
}

Three things to know before you turn it on:

  • There is no upload-only mode. enable_uploads is a setting on a share link, so the same URL still shows the board and its assets to whoever opens it. If the recipient should not see what is already there, share a fresh, empty board instead.
  • This endpoint returns the board's existing link. If the board was already shared for viewing, enabling uploads grants upload rights to everyone who was given that URL — it does not mint a separate one. uploads_enabled on the response tells you the state you just left the link in.
  • It requires a plan with shared-link customization. Other plans get 403 rather than a silently ignored field, so a link you were told was created is never quietly read-only.

Pair it with password and expiration_date whenever the link leaves a group you trust.

If all you hold is a share URL of your own workspace — say, one a teammate pasted into a brief — turn it back into the board behind it and read that board through the regular endpoints. Do not parse the share page: its embedded data can arrive empty, and an empty board then looks exactly like a page that has not loaded.

For https://playbook.com/s/my-org/creator-delivery, pass the last path segment:

curl "https://api.playbook.com/v1/my-org/shared_links/creator-delivery" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": {
"url": "https://playbook.com/s/my-org/creator-delivery",
"board_token": "main-collection",
"board_title": "Creator delivery",
"uploads_enabled": true,
"expired": false
}
}

Then list what is on it with GET /v1/my-org/assets?collection_token=main-collection&nested_assets=true.

  • Only active links of the workspace in the path resolve. A deactivated link, a link of another workspace, or one whose board is in the trash is a 404.
  • A link made by sharing individual assets (rather than a board) has no board to resolve and returns 422 with code unsupported_link.
  • A board's own address in the app, https://playbook.com/my-org/{board_token} with no /s/, is not a share link and needs no resolving: its last path segment already is the board token.

Over MCP this is resolve_share_link. It takes the full URL, reads the workspace from it, and accepts a board's own address too.


Publishing Boards​

Publish boards as public web galleries accessible to anyone with the link.

Creating a Published Board​

curl -X POST "https://api.playbook.com/v1/my-org/boards/portfolio/publish" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"disable_downloads": true}'

Response:

{
"data": {
"url": "https://playbook.com/p/my-org/portfolio"
}
}

Publishing with a Page Design​

Send template to give the page a designed layout. Valid ids are the keys of templates in GET /v1/page_layout_elements; an unknown id is refused with the list, and a page that already has a saved design answers 409 page_design_exists instead of being reset.

curl -X POST "https://api.playbook.com/v1/my-org/boards/portfolio/publish" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"template": "standard"}'

The design can then be read and edited element by element — see Published Page Design.

Every board response also reports the page's state as published_page: active, the template its design uses, and the public url (null unless the page is live).

Published vs. Shared​

FeatureShared LinkPublished Link
VisibilityPrivate (requires link)Public (indexable)
RevocableYesYes (can unpublish)
Password ProtectionYes (via UI)No
AnalyticsDetailedBasic
SEONoYes
Best ForClient reviews, approvalsPortfolios, galleries

Use Cases​

1. Client Review Process​

Share assets with clients for feedback:

async function sendForReview(assetTokens, clientEmail) {
// Create shared links for each asset
const sharedLinks = await Promise.all(
assetTokens.map((token) => shareAsset(token)),
);

// Send email with links
await emailService.send({
to: clientEmail,
subject: "Assets for Review",
body: `
Please review the following assets:

${sharedLinks.map((url, i) => `Asset ${i + 1}: ${url}`).join("\n")}

Reply with your feedback.
`,
});

return sharedLinks;
}

// Usage
await sendForReview(
["design-v1", "design-v2", "design-v3"],
);

2. Portfolio Website​

Embed published boards in your website:

<!-- Embed published board -->
<iframe
src="https://playbook.com/p/my-org/portfolio"
width="100%"
height="800px"
frameborder="0"
allowfullscreen
>
</iframe>

3. Temporary Access for Vendors​

Share boards with vendors for a limited time:

async function grantVendorAccess(boardToken, vendorEmail, expiryDays = 7) {
// Create shared link
const { url } = await shareBo ard(boardToken);

// Send with expiry notice
await emailService.send({
to: vendorEmail,
subject: 'Asset Access Granted',
body: `
You have access to our assets for the next ${expiryDays} days:
${url}

This link will be revoked after ${expiryDays} days.
`
});

// Schedule revocation (implement based on your system)
await scheduleRevocation(url, expiryDays);

return url;
}

4. Social Media Sharing​

Share individual assets on social platforms:

async function shareToSocialMedia(assetToken, platform) {
const shareUrl = await shareAsset(assetToken);

const asset = await getAsset(assetToken);

const shareText = encodeURIComponent(`Check out ${asset.title}: ${shareUrl}`);

const socialUrls = {
twitter: `https://twitter.com/intent/tweet?text=${shareText}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${shareUrl}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`,
};

return socialUrls[platform];
}

Downloading Shared Assets​

Shared links can include download capabilities. Get download URLs for shared assets:

For Shared Assets​

curl "https://api.playbook.com/v1/shared/my-org/assets/product-photo/download?sharedlinkslug=abc123" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": {
"display_url": "https://cdn.playbook.com/product-photo.jpg",
"raw_url": "https://cdn.playbook.com/product-photo-original.jpg",
"size": 2456789
}
}

JavaScript Example​

async function downloadSharedAsset(sharedOrgSlug, assetToken, sharedLinkSlug) {
const response = await fetch(
`https://api.playbook.com/v1/shared/${sharedOrgSlug}/assets/${assetToken}/download`,
{
headers: {
sharedlinkslug: sharedLinkSlug,
},
},
);

const { data } = await response.json();
return data.raw_url;
}

Implement your own tracking system:

class SharedLinkTracker {
constructor() {
this.links = new Map();
}

async createTrackedLink(assetOrBoardToken, type = "asset") {
// Create the share link
const url =
type === "asset"
? await shareAsset(assetOrBoardToken)
: await shareBoard(assetOrBoardToken);

// Store with metadata
this.links.set(url, {
token: assetOrBoardToken,
type,
createdAt: new Date(),
views: 0,
lastAccessed: null,
});

return url;
}

trackAccess(url) {
const link = this.links.get(url);
if (link) {
link.views++;
link.lastAccessed = new Date();
}
}

getAnalytics(url) {
return this.links.get(url);
}

listActiveLinks() {
const now = Date.now();
const dayAgo = now - 24 * 60 * 60 * 1000;

return Array.from(this.links.entries())
.filter(([_, link]) => link.lastAccessed > dayAgo)
.map(([url, link]) => ({ url, ...link }));
}
}

Revoking Access​

While there's no direct API to revoke shared links, you can:

  1. Delete the shared asset/board
  2. Move asset to different board
  3. Track and warn about expired links
async function revokeSharedAccess(assetToken) {
// Option 1: Move to private board
await updateAsset(assetToken, {
collection_token: "private-board",
});

// Option 2: Archive the asset
await updateAsset(assetToken, {
tags: [...asset.tags, "archived"],
});

console.log(`Access revoked for ${assetToken}`);
}

Best Practices​

1. Descriptive Sharing​

Include context when sharing:

async function shareWithContext(assetToken, recipient, message) {
const asset = await getAsset(assetToken);
const shareUrl = await shareAsset(assetToken);

await emailService.send({
to: recipient,
subject: `Shared Asset: ${asset.title}`,
body: `
${message}

Asset: ${asset.title}
Link: ${shareUrl}
Created: ${asset.created_at}
Tags: ${asset.tags.join(", ")}

This link will remain active until revoked.
`,
});
}

2. Organize Published Content​

Use consistent naming for published boards:

const publishedBoards = {
portfolio: "main-portfolio",
client_work: "client-showcase",
team: "team-photos",
};

async function publishAll() {
for (const [name, token] of Object.entries(publishedBoards)) {
const { url } = await publishBoard(token);
console.log(`Published ${name}: ${url}`);
}
}

3. Watermark Shared Assets​

Add watermarks to shared assets (implementation depends on your system):

async function shareWithWatermark(assetToken) {
// Get original asset
const asset = await getAsset(assetToken);

// Download and watermark
const watermarked = await addWatermark(asset.display_url, {
text: "CONFIDENTIAL",
opacity: 0.3,
});

// Upload watermarked version
const watermarkedAsset = await uploadAsset(watermarked);

// Share the watermarked version
return await shareAsset(watermarkedAsset.token);
}

4. Analytics and Monitoring​

Track sharing activity:

class SharingAnalytics {
async logShare(type, token, recipient) {
await analytics.track({
event: "asset_shared",
properties: {
type,
token,
recipient,
timestamp: new Date(),
},
});
}

async getMostShared(days = 30) {
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);

return await analytics.query({
event: "asset_shared",
since,
groupBy: "token",
orderBy: "count",
limit: 10,
});
}
}

Complete Sharing Workflow​

Here's a complete implementation with email and tracking:

class PlaybookSharingManager {
constructor(orgSlug, accessToken) {
this.orgSlug = orgSlug;
this.accessToken = accessToken;
this.baseUrl = "https://api.playbook.com/v1";
}

async shareAsset(assetToken, options = {}) {
const response = await fetch(
`${this.baseUrl}/${this.orgSlug}/assets/${assetToken}/share`,
{
method: "POST",
headers: { Authorization: `Bearer ${this.accessToken}` },
},
);

const { data } = await response.json();

// Track the share
if (options.trackingId) {
await this.logShare("asset", assetToken, options.trackingId);
}

return data.url;
}

async shareBoard(boardToken, options = {}) {
const response = await fetch(
`${this.baseUrl}/${this.orgSlug}/boards/${boardToken}/share`,
{
method: "POST",
headers: { Authorization: `Bearer ${this.accessToken}` },
},
);

const { data } = await response.json();

if (options.trackingId) {
await this.logShare("board", boardToken, options.trackingId);
}

return data.url;
}

async publishBoard(boardToken) {
const response = await fetch(
`${this.baseUrl}/${this.orgSlug}/boards/${boardToken}/publish`,
{
method: "POST",
headers: { Authorization: `Bearer ${this.accessToken}` },
},
);

return await response.json();
}

async shareWithEmail(type, token, recipients, message) {
// Create share link
const url =
type === "asset"
? await this.shareAsset(token, { trackingId: recipients.join(",") })
: await this.shareBoard(token, { trackingId: recipients.join(",") });

// Get asset/board info
const info = await this.getInfo(type, token);

// Send emails
for (const recipient of recipients) {
await this.sendShareEmail(recipient, url, info, message);
}

return url;
}

async getInfo(type, token) {
const endpoint = type === "asset" ? "assets" : "boards";
const response = await fetch(
`${this.baseUrl}/${this.orgSlug}/${endpoint}/${token}`,
{ headers: { Authorization: `Bearer ${this.accessToken}` } },
);

const { data } = await response.json();
return data;
}

async sendShareEmail(recipient, url, info, message) {
// Implement email sending
console.log(`Sending to ${recipient}:`, {
url,
title: info.title,
message,
});
}

async logShare(type, token, trackingId) {
// Log to your analytics system
console.log("Share logged:", { type, token, trackingId });
}
}

// Usage
const manager = new PlaybookSharingManager("my-org", "your_token");

// Share asset with email
await manager.shareWithEmail(
"asset",
"product-photo",
"Please review this product photo and provide feedback.",
);

// Publish board
const published = await manager.publishBoard("portfolio");
console.log("Portfolio published at:", published.data.url);


Next Steps​