Orders API v1 → v3 Migration Guide
This document provides a comprehensive migration guide from Orders API v1 to v3, including breaking changes, schema differences, and practical migration steps.
Overview
Orders API v3 introduces significant improvements in pagination, filtering, response shaping, and overall API consistency. While the core order types remain the same, the API structure has been modernized for better performance and developer experience.
Reference Documentation:
At-a-Glance Comparison
| Feature | v1 | v3 |
|---|---|---|
| Base path | /integration-api/v1/orders | /api/v3/orders |
| Authentication | X-User-Token header | X-User-Token header (unchanged) |
| Pagination | Page-based (page, last_page) | Cursor-based (links.next, meta.has_more) |
| Filtering | Query string params | Structured filter[field] syntax |
| Sorting | Not supported | sort parameter with direction |
| Response shaping | Fixed payload | include and fields parameters |
| Bulk operations | Sequential requests | Dedicated /orders/bulk endpoint |
| Date format | YYYY-MM-DD | ISO 8601 (YYYY-MM-DDTHH:mm:ss.000000Z) |
| External ID lookup | id_type=external query param | Same (unchanged) |
| ID fields | Transformed names | No transformations (matches DB columns) |
Breaking Changes
1. Endpoint Path Change
- GET /integration-api/v1/orders
+ GET /api/v3/orders
- GET /integration-api/v1/orders/{id}
+ GET /api/v3/orders/{id}
- POST /integration-api/v1/orders
+ POST /api/v3/orders
- PUT /integration-api/v1/orders/{id}
+ PUT /api/v3/orders/{id}
- DELETE /integration-api/v1/orders/{id}
+ DELETE /api/v3/orders/{id}
2. Pagination Change
v1 (Page-based):
{
"data": {
"paginator": {
"current_page": 1,
"data": [...],
"last_page": 10,
"per_page": 100,
"total": 950
}
}
}
v3 (Cursor-based):
{
"data": [...],
"links": {
"next": "https://yourstore.publica.la/api/v3/orders?cursor=eyJjcmVhdGVkX2F0...",
"prev": null
},
"meta": {
"has_more": true
}
}
v3 does not provide total count or last_page. Use meta.has_more to detect end of results.
3. Filter Syntax Change
v1:
GET /integration-api/v1/orders?status=approved&type=permission
v3:
GET /api/v3/orders?filter[status]=approved&filter[type]=permission
4. Response Structure Change
v1:
{
"data": {
"id": "275ca6c4-a815-4f64-8198-759a296dd495",
"external_reference": "ORDER-123",
"type": "permission",
"status": "approved",
"created_at": "2025-12-03",
"user": { ... },
"products": [ ... ]
}
}
v3 (base response):
{
"data": {
"id": 12345,
"uuid": "275ca6c4-a815-4f64-8198-759a296dd495",
"external_id": "ORDER-123",
"type": "permission",
"status": "approved",
"unit_price": 29.99,
"currency_id": "USD",
"created_at": "2025-12-03T22:43:44.000000Z",
"updated_at": "2025-12-03T22:43:44.000000Z"
}
}
In v3, user and products are not included by default. Use include=user,products to add them.
5. Field Name Changes (No Transformations)
v3 uses field names that match the database columns directly, without transformations:
| v1 Field | v3 Field | Description |
|---|---|---|
id (was uuid) | id (int) + uuid (string) | Both internal ID and UUID are now exposed |
external_reference | external_id | Renamed to match DB column |
user.id (was external_id) | user.id (int) + user.uuid + user.external_id | All user identifiers exposed |
products[].id (request) | products[].external_id (request) | Request field renamed |
products[].id (response) | products[].id (int) + products[].external_id | Both identifiers in response |
In v3 requests, use products[].external_id instead of products[].id:
- "products": [{ "type": "content", "id": "ISBN-123" }]
+ "products": [{ "type": "content", "external_id": "ISBN-123" }]
In responses, both id (internal) and external_id are returned.
v1 user object:
{
"user": {
"id": "user-ext-123",
"email": "[email protected]"
}
}
v3 user object:
{
"user": {
"id": 67890,
"uuid": "a1b2c3d4-e5f6-...",
"external_id": "user-ext-123",
"email": "[email protected]"
}
}
If your integration uses the id field from v1, note that:
- v1
id(order) = v3uuid - v1
user.id= v3user.external_id - v1
products[].id= v3products[].external_id
6. Date Format Change
v1:
{
"created_at": "2025-12-03",
"expiration_date": "2026-12-31"
}
v3:
{
"created_at": "2025-12-03T22:43:44.000000Z",
"updated_at": "2025-12-03T22:43:44.000000Z",
"expiration_date": "2026-12-31"
}
created_atandupdated_atare now full ISO 8601 timestampsexpiration_dateremains inYYYY-MM-DDformat
7. Product Response Fields
v1:
{
"products": [
{
"id": "9781234567890",
"type": "content",
"name": "Book Title",
"status": "approved",
"cover": "https://cdn.publica.la/covers/book.jpg",
"reader_url": "https://yourstore.publica.la/reader/book",
"description": "Book description",
"pages_quantity": 250,
"file_type": "epub",
"unit_price": 29.99,
"currency_id": "USD"
}
]
}
v3:
{
"products": [
{
"id": 111,
"external_id": "9781234567890",
"type": "content",
"name": "Book Title",
"status": "approved",
"expiration_date": null,
"unit_price": 29.99,
"currency_id": "USD",
"cover_url": "https://cdn.publica.la/covers/book.jpg",
"reader_url": "https://yourstore.publica.la/reader/book",
"product_url": "https://yourstore.publica.la/library/publication/book"
}
]
}
Key field changes:
idnow returns the internal numeric ID; useexternal_idfor the ISBN/external referencecover→cover_urldescription,pages_quantity,file_type→ not available (use Content API v3 for metadata)- New field:
product_url
The products[].id in v3 can be used directly with Content API v3:
GET /api/v3/content/111
Or use external_id with a filter:
GET /api/v3/content?filter[external_id]=9781234567890
8. Checkout URL for Sale Orders
v1:
{
"data": {
"checkout": {
"token": "https://yourstore.publica.la/auth/token?external-auth-token=eyJ0eXAi...",
"ttl": 3600
}
}
}
v3:
{
"data": {
"pending_checkout": {
"url": "https://yourstore.publica.la/auth/token?external-auth-token=eyJ0eXAi...",
"ttl": 3600
}
}
}
The checkout object has been renamed and restructured:
checkout→pending_checkoutcheckout.token→pending_checkout.url- In v3, requires
include=pending_checkoutto be returned
New Features in v3
1. Response Shaping with include
Request only the data you need:
GET /api/v3/orders?include=user,products
| Include | Description |
|---|---|
user | User information (id, uuid, external_id, email) |
products | Product details with URLs and pricing |
pending_checkout | Checkout URL for pending sale orders |
unit_price and currency_id are included in the base order response and in each product.
2. Sparse Fieldsets with fields
Reduce payload size by selecting specific fields:
GET /api/v3/orders?fields=id,status,type
3. Advanced Filtering
Filter by date ranges:
GET /api/v3/orders?filter[created_at][from]=2025-01-01 00:00:00&filter[created_at][to]=2025-12-31 23:59:59
Filter by user:
GET /api/v3/orders?filter[user_id]=12345 # By internal ID
GET /api/v3/orders?filter[user_external_id]=user-123 # By external ID
GET /api/v3/orders?filter[user_email][email protected] # By email
Filter by product:
GET /api/v3/orders?filter[product_id]=67890 # By internal ID
GET /api/v3/orders?filter[product_external_id]=9781234567890 # By external ID (ISBN, SKU)
GET /api/v3/orders?filter[product_type]=content # By type
4. Sorting
GET /api/v3/orders?sort=-created_at # Newest first (default)
GET /api/v3/orders?sort=created_at # Oldest first
GET /api/v3/orders?sort=-updated_at # Recently updated first
5. Bulk Operations
Create up to 100 orders in a single request:
POST /api/v3/orders/bulk
{
"orders": [
{ "type": "permission", ... },
{ "type": "permission", ... }
]
}
Migration Checklist
1. Update Endpoints
Replace all v1 endpoint paths with v3 paths:
| Operation | v1 | v3 |
|---|---|---|
| List | /integration-api/v1/orders | /api/v3/orders |
| Get | /integration-api/v1/orders/{id} | /api/v3/orders/{id} |
| Create | /integration-api/v1/orders | /api/v3/orders |
| Bulk Create | N/A | /api/v3/orders/bulk |
| Update | /integration-api/v1/orders/{id} | /api/v3/orders/{id} |
| Delete | /integration-api/v1/orders/{id} | /api/v3/orders/{id} |
2. Update Pagination Logic
Before (v1):
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`/integration-api/v1/orders?page=${page}`);
const data = await response.json();
processOrders(data.data.paginator.data);
hasMore = page < data.data.paginator.last_page;
page++;
}
After (v3):
let cursor = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? `/api/v3/orders?cursor=${cursor}`
: '/api/v3/orders';
const response = await fetch(url);
const data = await response.json();
processOrders(data.data);
hasMore = data.meta.has_more;
cursor = data.links.next ? new URL(data.links.next).searchParams.get('cursor') : null;
}
3. Update Filter Syntax
Before (v1):
const params = new URLSearchParams({
status: 'approved',
type: 'permission'
});
After (v3):
const params = new URLSearchParams({
'filter[status]': 'approved',
'filter[type]': 'permission'
});
4. Add Includes for Full Data
If you need user and product data, add includes:
Before (v1):
GET /integration-api/v1/orders
After (v3):
GET /api/v3/orders?include=user,products
Note: unit_price and currency_id are always included in the base response.
5. Update Field Mappings
Update your code to handle renamed and new fields:
// v1 → v3 order field mapping
const mapOrder = (v1Order, v3Order) => ({
// v1 'id' (uuid string) is now v3 'uuid'
id: v3Order.uuid, // or use v3Order.id for numeric ID
external_reference: v3Order.external_id, // renamed from external_reference
// ... other fields
});
// v1 → v3 user field mapping
const mapUser = (v1User, v3User) => ({
// v1 'id' (external_id string) is now v3 'external_id'
id: v3User.external_id, // or use v3User.id for numeric ID
email: v3User.email,
// New fields in v3:
// uuid: v3User.uuid
});
// v1 → v3 product field mapping
const mapProduct = (v1Product, v3Product) => ({
// v1 'id' (external_id string) is now v3 'external_id'
id: v3Product.external_id, // or use v3Product.id for numeric ID
type: v3Product.type,
name: v3Product.name,
status: v3Product.status,
expiration_date: v3Product.expiration_date,
cover: v3Product.cover_url, // renamed from cover
reader_url: v3Product.reader_url,
unit_price: v3Product.unit_price,
currency_id: v3Product.currency_id,
// New field in v3:
// product_url: v3Product.product_url
});
6. Update Date Parsing
// v1 dates
const v1Date = "2025-12-03";
const parsed = new Date(v1Date);
// v3 dates (ISO 8601)
const v3Date = "2025-12-03T22:43:44.000000Z";
const parsed = new Date(v3Date); // Works the same, but includes time
7. Test Error Handling
v3 returns more detailed validation errors:
{
"message": "The given data was invalid.",
"errors": {
"filter.status": ["Invalid status. Allowed: pending, approved, paused, cancelled"],
"include": ["Invalid include: invalid. Allowed: user, products, pending_checkout"]
}
}
Removed Features
| Feature | v1 | v3 Alternative |
|---|---|---|
total count | Available in paginator | Not available; use streaming pagination |
last_page | Available in paginator | Not available; use meta.has_more |
description in products | Direct field | Use Content API v3 |
pages_quantity in products | Direct field | Use Content API v3 |
file_type in products | Direct field | Use Content API v3 |
external_reference | Field name | Renamed to external_id |
Practical Examples
List Orders with Full Data (v3)
curl -X GET "https://yourstore.publica.la/api/v3/orders?include=user,products&per_page=100" \
-H "X-User-Token: your-api-token"
Incremental Sync by Update Date (v3)
curl -X GET "https://yourstore.publica.la/api/v3/orders?\
filter[updated_at][from]=2025-12-01 00:00:00&\
filter[updated_at][to]=2025-12-03 23:59:59&\
sort=-updated_at&\
include=user,products" \
-H "X-User-Token: your-api-token"
Create Order (v3)
curl -X POST "https://yourstore.publica.la/api/v3/orders" \
-H "X-User-Token: your-api-token" \
-H "Content-Type: application/json" \
-d '{
"type": "permission",
"external_reference": "ORDER-123",
"user": {
"id": "user-123",
"email": "[email protected]"
},
"products": [
{
"type": "content",
"external_id": "9781234567890"
}
]
}'
Get Order by External Reference (v3)
curl -X GET "https://yourstore.publica.la/api/v3/orders/ORDER-123?id_type=external&include=user,products" \
-H "X-User-Token: your-api-token"
Troubleshooting FAQ
Why is my response missing user/products data?
In v3, user and products are not included by default. Add include=user,products to your request.
How do I get the total count of orders?
v3 uses cursor pagination without total counts. For counting, make a separate count query or track counts in your application.
Why am I getting 422 errors?
Check:
- Filter syntax:
filter[field]=value, notfield=value - Date format:
YYYY-MM-DD HH:mm:ssfor date ranges - Include values: only
user,products,pending_checkoutare valid
How do I migrate bulk imports?
Use the new /api/v3/orders/bulk endpoint instead of sequential requests. It's faster and supports up to 100 orders per request.
Where is the product description?
Product metadata (description, page count, file type) is not included in the Orders API v3 response. Use the Content API v3 to get full product details:
GET /api/v3/content/{products[].id}
What happened to external_reference?
The field has been renamed to external_id to match the database column name. Update your code to use external_id instead.
How do I map v1 IDs to v3?
| v1 Field | v3 Equivalent |
|---|---|
Order id | uuid |
Order external_reference | external_id |
User id | external_id |
Product id | external_id |
Parallel Running Strategy
For a safe migration:
- Feature flag: Route traffic to v1 or v3 based on a feature flag
- Dual writes: If creating orders, consider writing to both APIs initially
- Compare responses: Log and compare v1 vs v3 responses for the same queries
- Gradual rollout: Start with read-only operations, then move to writes
- Monitor: Track error rates, latencies, and payload sizes
References
- Orders API v3 Overview
- List Orders (v3)
- Get Order (v3)
- Permission Orders (v3)
- Report Orders (v3)
- Sale Orders (v3)
- Bulk Operations (v3)
- Orders API v1 (Deprecated)