Overview
The Palixo Sync API provides a set of endpoints for synchronizing album and picture data between mobile clients and the server. The API supports bidirectional sync with conflict resolution, incremental updates, and efficient image upload/download via presigned URLs.
Key Features
- Pull-based sync: Fetch changes since a specific timestamp
- Push-based mutations: Create, update, upsert, and delete entities
- Pagination: Handle large datasets with continuation tokens
- Idempotency: Safe retry of mutations using idempotency keys
- Presigned URLs: Direct S3 upload/download without Lambda payload limits
- Multi-entity support: Sync albums, pictures, tags, and access control
Base URL
| Environment | URL |
|---|---|
| UAT | https://uat.palixo.media/sync |
| Production | https://palixo.media/sync |
Authentication
All endpoints require authentication using JWT ID tokens from AWS Cognito User Pool.
Cognito Configuration
| Setting | UAT Environment | Production Environment |
|---|---|---|
| Region | ap-southeast-2 | ap-southeast-2 |
| User Pool ID | Contact administrator | Contact administrator |
| App Client ID | Contact administrator | Contact administrator |
Using the ID Token
Include the ID Token (not access token) in all API requests:
Authorization: Bearer <JWT_ID_TOKEN> Token Expiration
- ID Token: Expires in 1 hour (3600 seconds)
- Access Token: Expires in 1 hour (3600 seconds)
- Refresh Token: Expires in 30 days
Note: Clients should proactively refresh tokens before expiration to avoid authentication failures during sync operations.
Common Headers
| Header | Required | Description |
|---|---|---|
Authorization | Required | JWT Bearer token from Cognito |
Content-Type | Required (POST) | Must be application/json |
X-Device-Id | Optional | Device identifier for conflict tracking |
X-Idempotency-Key | Optional | Unique key for idempotent push operations |
API Endpoints
Pull Changes
Fetch changes from the server since a specific timestamp.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
since | string | Optional | ULID sequence identifier. Returns all changes if omitted |
albumId | string | Optional | Filter changes to a specific album |
limit | number | Optional | Max changes to return (default: 100, max: 1000) |
continuationToken | string | Optional | Base64-encoded pagination token |
Response Example
{
"changes": [
{
"id": "<ksuid>",
"v": 1,
"entityType": "Picture",
"data": {
"pictureId": "<ksuid>",
"albumId": "<albumId>",
"fileName": "Sunset.jpg",
"created": "[ksuid]",
"modified": "[ksuid]"
}
}
],
"last_seq": "[ulid]",
"albums": [
{
"albumId": "<albumId>",
"name": "Vacation 2025",
"accessLevel": "OWNER"
}
],
"hasMore": false
} Push Mutations
Send create, update, upsert, or delete mutations to the server.
Request Example
{
"albumId": "<albumId>",
"mutations": [
{
"id": "<ksuid>",
"op": "create",
"data": {
"entityType": "Picture",
"fileName": "Beach.jpg",
"timestamp": "[ksuid]",
"created": "[ksuid]"
}
}
]
} Mutation Operations
| Operation | Description |
|---|---|
create | Create a new entity. Pictures: use id with client-generated KSUID. Other entities: use temp_id |
update | Update an existing entity. Use server-assigned id |
upsert | Create if doesn't exist, update if exists |
delete | Delete an entity. Use server-assigned id |
Auto-Tag Creation Feature
When creating a PrivatePictureTag that references a tag using a temp_id, the server will automatically create the PrivateTag entity if it doesn't exist. The response will include multiple results: one for the auto-created tag and one for the picture-tag relation.
Get Upload URL
Get a presigned S3 URL for uploading an image.
Response Example
{
"uploadUrl": "https://s3.amazonaws.com/bucket/<pictureId>?X-Amz-Algorithm=...",
"expiresIn": 300
} Download Image
Get a presigned S3 URL for downloading an image.
Response Example
{
"downloadUrl": "https://s3.amazonaws.com/bucket/<pictureId>?X-Amz-Algorithm=...",
"expiresIn": 300
} Bulk Download Images
Get presigned S3 URLs for multiple images in a single request (max 100).
Request Example
{
"pictureIds": [
"<pictureId_1>",
"<pictureId_2>",
"<pictureId_3>"
]
} Health Check
Check API health status.
Response Example
{
"status": "healthy",
"service": "sync",
"timestamp": "2025-11-26T..."
} Data Types
Entity Types
| Entity Type | Description |
|---|---|
Album | Photo album container |
Picture | Individual photo/image |
PrivateTag | User-created tag for organizing pictures |
PrivatePictureTag | Association between a picture and a tag |
AlbumAccess | User's access permissions to an album |
User | User account information |
Picture Entity Fields
| Field | Type | Required | Description |
|---|---|---|---|
pictureId | string | Required | Client-generated KSUID (also serves as the S3 object key) |
albumId | string | Required | ID of the album containing this picture |
fileName | string | Required | Original file name (e.g., "IMG_1234.jpg") |
timestamp | string | Required | KSUID representing when the photo was captured |
created | string | Required | KSUID timestamp when entity was created |
modified | string | Required | KSUID timestamp when entity was last modified (server-managed) |
nameOverride | string | Optional | Display name that overrides fileName |
description | string | Optional | Description or caption for the picture |
location | string | Optional | GPS location or location description |
imageVersion | string | Optional | Version string for tracking edits/updates |
KSUID vs ULID
| Type | Precision | Usage |
|---|---|---|
| KSUID | Second-level | Entity IDs, created and modified timestamps |
| ULID | Millisecond-level | Sync sequence tracking only (last_seq, since) |
Special Album ID: Use "global" as the albumId for user-scoped entities like PrivateTag that aren't associated with a specific album.
Common Workflows
Initial Sync
- Call
GET /sync/pullwithoutsinceparameter to fetch all data - Process
albumsarray to build local album list - Process
changesarray to populate local database - Store
last_seqfor incremental sync - If
hasMoreis true, repeat withcontinuationToken
Incremental Sync
- Call
GET /sync/pull?since={last_seq}using stored sequence - Process
changesarray to update local database - Update stored
last_seqwith response value - Handle pagination if
hasMoreis true
Create Picture with Upload
- Generate unique KSUID for the picture ID (serves as both entity ID and S3 key)
- Call
POST /sync/pushto create picture entity - Verify successful creation from result
- Call
POST /sync/get-upload-url/:pictureId - Upload image to S3 using presigned URL
Error Handling
HTTP Status Codes
| Code | Description |
|---|---|
200 OK | Request successful |
400 Bad Request | Invalid request parameters or body |
401 Unauthorized | Missing or invalid authentication token |
403 Forbidden | Authenticated but not authorized for resource |
404 Not Found | Resource not found |
500 Internal Server Error | Server-side error |
Error Response Format
{
"success": false,
"message": "Error description",
"error": "Detailed error message"
} Client Implementation Notes
This section documents important patterns and considerations for implementing a robust sync client.
Document Processing Order
The client should process entity types in a specific order to satisfy foreign key dependencies:
- Album - Process first as other entities reference albums
- Picture - Process second as tags reference pictures
- PrivateTag - Process third as picture-tags reference tags
- PrivatePictureTag - Process last as it references both pictures and tags
Important: When processing changes from a pull response, entities should be applied in this dependency order to avoid foreign key constraint violations in your local database.
Duplicate Detection
Server pagination may occasionally return duplicate entities across pages. Clients should track processed entities using a composite key of entityType:id to detect and skip duplicates:
// Pseudocode
seenEntities = Set()
for each change in response.changes:
key = change.entityType + ":" + change.id
if key in seenEntities:
skip // Duplicate, already processed
seenEntities.add(key)
processChange(change) Pagination Safety
Implement safety limits to prevent infinite loops in case of server pagination bugs:
- Set a maximum page count (e.g., 100 pages)
- Detect if continuation token is unchanged between requests
- Break pagination loop if all entities on a page are duplicates
Push Batching by Album
When pushing mutations, group them by albumId and send separate requests for each album. Push the global album first to ensure user-scoped entities (like tags) are created before album-specific entities that reference them.
Temp ID Replacement
When pushing multiple related mutations in sequence, maintain a mapping of temp IDs to server-assigned IDs. Before sending each batch, scan the mutation data and replace any temp ID references with their real IDs from previous responses:
// Pseudocode
tempIdToRealId = Map()
for each batch in pendingMutations:
// Replace temp IDs in data fields
for each mutation in batch:
for field in ['pictureId', 'privateTagId', 'albumId']:
if mutation.data[field] in tempIdToRealId:
mutation.data[field] = tempIdToRealId[mutation.data[field]]
// Send batch and process results
response = push(batch)
for result in response.results:
if result.temp_id:
tempIdToRealId[result.temp_id] = result.id Best Practices
Authentication
- Always include authentication headers
- Implement token refresh before expiration
- Store tokens securely
Idempotency
- Always use
X-Idempotency-Keyfor push requests - Generate unique keys per logical operation
- Retry failed requests with same key
Performance
- Use bulk operations (
download-images) for multiple resources - Implement local caching of presigned URLs (respect
expiresIn) - Batch mutations into single push requests when possible
Data Integrity
- Validate entity data before pushing
- Handle deleted entities in pull responses
- Implement version-based conflict detection
- Store
last_seqpersistently for crash recovery