Appearance
Project Management
API Type
PWA Backend API - Uses access keys in X-Albumstory header. See Authentication.
Manage user projects: list, delete, duplicate, and transfer ownership.
Environment URLs:
- Development:
https://pwa-api-dev.photobook.ai/v2/* - Production:
https://pwa-api.photobook.ai/v2/*
List User Projects
Retrieve all projects for a specific user.
Endpoint
GET https://pwa-api{-env}.photobook.ai/v2/project/list?user={userId}Authentication
See Authentication - PWA Backend
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
user | string | Yes | End user's identifier (userId or email used during creation) |
cursor | string | No | Pagination token from previous response. Fetches next page of results |
Response
json
{
"data": [
{
"title": "Summer Memories 2024",
"preview": "https://cdn.photobook.ai/previews/abc123.jpg",
"id": "p-abc123xyz",
"status": {
"photos": "idle",
"project": "editable"
},
"createdAt": 1713190200000,
"updatedAt": 1713276600000,
"expiresAt": 1715868600,
"product": {
"psp": "PrinterName",
"sku": "PB001",
"marketId": 0,
"marketIdAlt": "custom-market"
},
"link": "https://{frontend}.photobook.ai?id=p-abc123xyz",
"ordered": false
}
],
"cursor": "next-pagination-token"
}Response Fields
| Field | Type | Description |
|---|---|---|
data | array | List of user's projects |
cursor | string | A pagination cursor. If defined, suggests that there is another page to fetch |
Project Object
| Field | Type | Description |
|---|---|---|
title | string | Project title (inferred from cover text, falls back to SKU) |
preview | string | Preview image URL (empty if not saved yet) |
id | string | Project ID (starts with p-) |
status | object | Project and photo status |
status.photos | string | Photo status: none, uploading, analyzing, idle |
status.project | string | Project status: created, building, editable, deleted |
createdAt | integer | Unix timestamp in milliseconds |
updatedAt | integer | Unix timestamp in milliseconds |
expiresAt | integer | Unix timestamp in seconds (when project will be deleted) |
product | object | Product details |
product.psp | string | Printer name |
product.sku | string | Product SKU |
product.marketId | integer | Internal market ID |
product.marketIdAlt | string | Alternative market identifier |
link | string | Direct link to project in PWA editor |
ordered | boolean | Whether project has been ordered |
Pagination
The API returns a maximum of 100 orders per request. Use the cursor field to fetch additional pages.
How Pagination Works
- Make initial request without
cursor - Display the 100 results
- If response includes a
cursor, there are more results - Make next request with the
cursorvalue - Repeat until
cursoris undefined
⚠️ Important
datamay be empty even ifcursoris returned- Always check for
cursorto determine if more pages exist - Continue fetching as long as
cursoris present
Example
javascript
async function getUserProjects(userId) {
const response = await fetch(
`https://pwa-api-dev.photobook.ai/v2/project/list?user=${encodeURIComponent(userId)}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`,
{
method: 'GET',
headers: {
'X-Albumstory': JSON.stringify({
accessKey: process.env.PWA_ACCESS_KEY,
accessSecret: process.env.PWA_ACCESS_SECRET
})
}
}
);
const data = await response.json();
return data.data;
}Get Project Status
Retrieve the current status of a specific project.
Endpoint
GET https://pwa-api{-env}.photobook.ai/v2/project/status?id={projectId}Authentication
See Authentication - PWA Backend
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Project ID (starts with p-) |
Response
json
{
"id": "p-abc123xyz",
"status": {
"photos": "idle",
"project": "editable",
"order": "delivering"
},
"user": {
"firstName": "John",
"lastName": "Doe",
"phone": "+1512345678",
"shippingAddress": {
"country": "United States",
"city": "Jacksonville",
"isoCode2": "US",
"isoCode3": "USA",
"state": "Florida",
"postal": "12345",
"line1": "123 Florida Way"
},
"userId": "johndoe",
"email": "random@photobook.ai",
"actualEmail": "johndoe@johndoe.net"
},
"tracking": "https://www.ups.com/track?loc=en_US&requester=QUIC%2F/trackdetails&tracknum=1ABC1234"
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Project ID |
status | object | Project and photo status |
status.photos | string | Photo status: none, uploading, analyzing, idle |
status.project | string | Project status: created, building, editable, deleted |
status.order | string | undefined | Fulfillment status (e.g. delivering); only present after the project has been sent for printing |
user | object | undefined | End user details; only present after the project has been sent for printing |
user.userId | string | User's internal identifier |
user.email | string | undefined | User's email address |
user.firstName | string | undefined | User's first name |
user.lastName | string | undefined | User's last name |
user.phone | string | undefined | User's phone number |
user.shippingAddress | object | User's shipping address |
user.shippingAddress.line1 | string | Address line 1 |
user.shippingAddress.city | string | City |
user.shippingAddress.state | string | State or region |
user.shippingAddress.postal | string | Postal / ZIP code |
user.shippingAddress.country | string | Country name |
user.shippingAddress.isoCode2 | string | 2-letter ISO country code |
user.shippingAddress.isoCode3 | string | 3-letter ISO country code |
tracking | string | undefined | Carrier tracking URL; only present when status.order is delivering, and may not always be available |
Status Meanings
Photo Status
| Value | Description |
|---|---|
none | No photos uploaded |
uploading | Photos currently uploading |
analyzing | Photos being analyzed |
idle | Photos ready (not uploading or analyzing) |
Project Status
| Value | Description |
|---|---|
created | Project created, book data not yet generated |
building | Book data being generated |
editable | Ready for user editing |
ordering | Project has been submitted for ordering |
expired | Project has expired and is no longer accessible |
deleted | Marked for deletion (data expunged after 2 days) |
Order Status
INFO
Statuses after transferring to GPS (PBAI's Global Print System) for fulfillment with PSPs/Printers
| Value | Description |
|---|---|
waiting | Order item created; assets and payment not yet made |
expiring | Order item created but still waiting; will expire soon |
last call | Order item in expiring state; last chance to complete |
files missing | Payment made but assets not uploaded after timeout |
received | Assets uploaded |
paid | Payment received |
processing | Payment and assets complete; being processed |
rendering | Rendering final assets |
rendered | Final assets rendered |
sending printjob | Sending print job to PSP |
sent printjob | Print job sent to PSP |
accepted | PSP received and accepted the order |
printing | PSP is printing the order |
printed | PSP has printed the order |
delivering | Order is now being delivered (tracking URL may be available) |
complete | Order fully completed |
ready for collection | Ready for collection / pickup |
collected | Order collected |
abnormal | Abnormal delivery reported by PSP |
failed processing | Failed processing reported by PSP |
on hold | Order placed on hold by PSP |
undelivered | Courier was unable to deliver the package |
expired | Courier stopped tracking; no new updates received |
cleaned | Order assets removed due to age |
cancelled | Order cancelled by eCommerce |
refunded | Order refunded by eCommerce |
abandoned | Paid but assets not uploaded after final timeout, or passed last call |
deleted | Order manually deleted |
Example
javascript
async function getProjectStatus(projectId) {
const response = await fetch(
`https://pwa-api-dev.photobook.ai/v2/project/status?id=${encodeURIComponent(projectId)}`,
{
method: 'GET',
headers: {
'X-Albumstory': JSON.stringify({
accessKey: process.env.PWA_ACCESS_KEY,
accessSecret: process.env.PWA_ACCESS_SECRET
})
}
}
);
return await response.json();
}Delete Projects
Delete one or more projects for a user.
Endpoint
POST https://pwa-api{-env}.photobook.ai/v2/project/deleteRequest Parameters
json
{
"user": "user-12345",
"type": "async",
"ids": [
"p-abc123xyz",
"p-def456uvw"
]
}| Parameter | Type | Required | Description |
|---|---|---|---|
user | string | Yes | End user's identifier |
type | string | Yes | Deletion mode: async or sync |
ids | array | Yes | Project IDs to delete (max 10 per request) |
Deletion Types
async- Initiates deletion and returns immediately (recommended)sync- Waits for deletion to complete before responding
Max Projects
Maximum of 10 project IDs per API call.
Response
json
{
"message": "scheduled to delete p-abc123xyz, p-def456uvw"
}Example
javascript
async function deleteProjects(userId, projectIds) {
const response = await fetch('https://pwa-api-dev.photobook.ai/v2/project/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Albumstory': JSON.stringify({
accessKey: process.env.PWA_ACCESS_KEY,
accessSecret: process.env.PWA_ACCESS_SECRET
})
},
body: JSON.stringify({
user: userId,
type: 'async',
ids: projectIds.slice(0, 10) // Ensure max 10
})
});
return await response.json();
}Duplicate Project
Create a copy of an existing project for the same user.
Endpoint
POST https://pwa-api{-env}.photobook.ai/v2/project/duplicateRequest Parameters
json
{
"id": "p-abc123xyz",
"callback": "https://your-app.com/api/order-callback"
}| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Project ID to duplicate |
callback | string | No | Override callback URL for the duplicated project |
Response
json
{
"title": "Summer Memories 2024",
"preview": "https://cdn.photobook.ai/previews/new123.jpg",
"id": "p-new123xyz",
"endUserId": "user-12345",
"createdAt": 1713190200000,
"updatedAt": 1713190200000,
"expiresAt": 1715868600,
"product": {
"sku": "PB001",
"psp": "PrinterName",
"marketId": 0,
"marketIdAlt": "custom-market"
},
"link": "https://{frontend}.photobook.ai?id=p-new123xyz"
}Example
javascript
async function duplicateProject(projectId, newCallback = null) {
const requestData = { id: projectId };
if (newCallback) {
requestData.callback = newCallback;
}
const response = await fetch('https://pwa-api-dev.photobook.ai/v2/project/duplicate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Albumstory': JSON.stringify({
accessKey: process.env.PWA_ACCESS_KEY,
accessSecret: process.env.PWA_ACCESS_SECRET
})
},
body: JSON.stringify(requestData)
});
const data = await response.json();
return data.id; // Return new project ID
}Transfer Project
Transfer project ownership to another user.
Endpoint
POST https://pwa-api{-env}.photobook.ai/v2/project/transferRequest Parameters
json
{
"id": "p-abc123xyz",
"targetUser": "new-user-12345",
"callback": "https://your-app.com/api/order-callback"
}| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Project ID to transfer |
targetUser | string | Yes | Target user identifier to transfer ownership to |
callback | string | No | Override callback URL for the transferred project |
Response
json
{
"title": "Summer Memories 2024",
"preview": "https://cdn.photobook.ai/previews/abc123.jpg",
"id": "p-abc123xyz",
"endUserId": "new-user-12345",
"createdAt": 1713190200000,
"updatedAt": 1713276600000,
"expiresAt": 1715868600,
"product": {
"sku": "PB001",
"psp": "PrinterName",
"marketId": 0,
"marketIdAlt": "custom-market"
},
"link": "https://{frontend}.photobook.ai?id=p-abc123xyz"
}⚠️
After transfer, the endUserId reflects the new owner. The original user will no longer see this project in their list.
Example
javascript
async function transferProject(projectId, newUserId) {
const response = await fetch('https://pwa-api-dev.photobook.ai/v2/project/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Albumstory': JSON.stringify({
accessKey: process.env.PWA_ACCESS_KEY,
accessSecret: process.env.PWA_ACCESS_SECRET
})
},
body: JSON.stringify({
id: projectId,
targetUser: newUserId
})
});
return await response.json();
}Use Cases
Display User's Projects
javascript
async function displayProjects(userId) {
const projects = await getUserProjects(userId);
// Filter by status
const editableProjects = projects.filter(p =>
p.status.project === 'editable' && !p.ordered
);
// Sort by recently updated
editableProjects.sort((a, b) => b.updatedAt - a.updatedAt);
return editableProjects;
}Cleanup Old Projects
javascript
async function cleanupExpiredProjects(userId) {
const projects = await getUserProjects(userId);
const now = Date.now() / 1000; // Convert to seconds
// Find projects expiring soon (within 7 days)
const expiringSoon = projects.filter(p =>
p.expiresAt - now < 7 * 24 * 60 * 60
);
if (expiringSoon.length > 0) {
console.log(`${expiringSoon.length} projects expiring soon`);
}
}Batch Delete
javascript
async function batchDeleteProjects(userId, projectIds) {
// Delete in batches of 10
const batches = [];
for (let i = 0; i < projectIds.length; i += 10) {
batches.push(projectIds.slice(i, i + 10));
}
for (const batch of batches) {
await deleteProjects(userId, batch);
}
}Suggested Practices
- Check Status - Use
status.projectto determine if project is ready for editing - Handle Expiration - Display expiration warnings to users with upcoming
expiresAtdates - Async Deletion - Use
asyncdeletion type for better performance - Batch Wisely - When deleting multiple projects, respect the 10-project limit