Orders API
The Orders API allows customers to track their gallery purchases. Orders contain items from multiple artists with independent fulfillment, so each order may have multiple shipments.
Overview
Gallery orders work differently from traditional e-commerce:
- Multi-artist: One order can contain items from many artists
- Per-artist fulfillment: Each artist ships their items independently
- Cascading status: Parent order status updates based on artist statuses
- Public tracking: No authentication required (uses public order ID)
Endpoints
Get Order by ID
Retrieves complete order details including all artist shipments.
Endpoint: GET /api/gallery/orders/{publicId}
Authentication: None (public order ID acts as credential)
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
publicId | string | Public order ID (format: GAL-YYYY-XXXXXX) |
Response:
{
"order": {
"id": "order_abc123",
"publicId": "GAL-2024-ABC123",
"status": "partially_shipped",
"total": 230.80,
"subtotal": 200.00,
"galleryFee": 24.00,
"stripeFee": 6.80,
"email": "buyer@example.com",
"shippingOption": "single_address",
"shippingAddress": {
"name": "John Doe",
"line1": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
},
"createdAt": "2024-01-15T10:30:00Z",
"paidAt": "2024-01-15T10:32:00Z",
"artistOrders": [
{
"id": "artist_order_001",
"orgId": "org_abc",
"artistName": "Jane Smith Studio",
"status": "shipped",
"subtotal": 120.00,
"artistPayout": 105.60,
"trackingNumber": "1Z999AA10123456784",
"trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
"shippedAt": "2024-01-16T14:20:00Z",
"items": [
{
"id": "item_001",
"productId": "prod_001",
"productName": "Abstract Painting #5",
"quantity": 2,
"price": 60.00,
"image": "https://..."
}
]
},
{
"id": "artist_order_002",
"orgId": "org_def",
"artistName": "Bob's Pottery",
"status": "processing",
"subtotal": 80.00,
"artistPayout": 70.40,
"trackingNumber": null,
"trackingUrl": null,
"shippedAt": null,
"items": [
{
"id": "item_002",
"productId": "prod_002",
"productName": "Ceramic Vase",
"quantity": 1,
"price": 80.00,
"image": "https://..."
}
]
}
]
}
}
Response Fields:
Order Object
| Field | Type | Description |
|---|---|---|
id | string | Internal order UUID |
publicId | string | Public-facing order ID |
status | string | Overall order status (see statuses below) |
total | number | Total amount charged (USD) |
subtotal | number | Sum of all product prices |
galleryFee | number | 12% gallery commission |
stripeFee | number | 2.9% + $0.30 payment processing |
email | string | Customer email |
shippingOption | string | "single_address" or "per_artist" |
shippingAddress | object | Shipping address (if single_address) |
createdAt | string | ISO 8601 timestamp |
paidAt | string | ISO 8601 timestamp |
artistOrders | array | Per-artist order breakdowns |
Artist Order Object
| Field | Type | Description |
|---|---|---|
id | string | Artist order UUID |
orgId | string | Artist organization ID |
artistName | string | Artist display name |
status | string | Artist order status |
subtotal | number | Total for this artist's items |
artistPayout | number | Amount artist receives (88%) |
trackingNumber | string | Shipment tracking number (if shipped) |
trackingUrl | string | Carrier tracking URL (if shipped) |
shippedAt | string | ISO 8601 timestamp (if shipped) |
items | array | Line items for this artist |
Example Request:
curl https://gallery.artbase.studio/api/gallery/orders/GAL-2024-ABC123
Error Responses:
| Status | Error | Description |
|---|---|---|
| 404 | Order not found | Invalid public ID |
Order Statuses
Gallery orders have cascading statuses based on artist fulfillment:
| Status | Description | Criteria |
|---|---|---|
pending | Awaiting payment | PaymentIntent created but not paid |
paid | Payment successful | PaymentIntent succeeded, no shipments yet |
processing | Being prepared | At least one artist order is processing |
partially_shipped | Partial shipment | Some but not all artist orders shipped |
shipped | Fully shipped | All artist orders shipped |
delivered | Fully delivered | All artist orders delivered |
cancelled | Cancelled | Order cancelled before any shipment |
Status Flow
pending → paid → processing → partially_shipped → shipped → delivered
↓
cancelled
Artist Order Statuses
Each artist order has its own status:
| Status | Description |
|---|---|
pending | Awaiting payment |
paid | Payment received, not yet preparing |
processing | Artist is preparing items |
shipped | Artist has shipped items |
delivered | Items delivered to customer |
Cascading Logic
The parent order status is calculated from all artist orders:
function calculateOrderStatus(artistOrders) {
const allDelivered = artistOrders.every(ao => ao.status === 'delivered');
const allShipped = artistOrders.every(ao => ao.status === 'shipped' || ao.status === 'delivered');
const anyShipped = artistOrders.some(ao => ao.status === 'shipped' || ao.status === 'delivered');
const anyProcessing = artistOrders.some(ao => ao.status === 'processing');
if (allDelivered) return 'delivered';
if (allShipped) return 'shipped';
if (anyShipped) return 'partially_shipped';
if (anyProcessing) return 'processing';
return 'paid';
}
Tracking Numbers
When an artist ships items, they provide a tracking number:
Supported Carriers
The system auto-generates tracking URLs for major carriers:
| Carrier | Pattern | Tracking URL |
|---|---|---|
| UPS | 1Z... | https://www.ups.com/track?tracknum={number} |
| USPS | Varies | https://tools.usps.com/go/TrackConfirmAction?tLabels={number} |
| FedEx | Varies | https://www.fedex.com/fedextrack/?trknbr={number} |
Tracking URL Generation
function generateTrackingUrl(trackingNumber: string): string {
if (trackingNumber.startsWith('1Z')) {
return `https://www.ups.com/track?tracknum=${trackingNumber}`;
} else if (/^\d{20,22}$/.test(trackingNumber)) {
return `https://tools.usps.com/go/TrackConfirmAction?tLabels=${trackingNumber}`;
} else {
return `https://www.fedex.com/fedextrack/?trknbr=${trackingNumber}`;
}
}
Shipping Address Handling
Single Address
When shippingOption: "single_address", all artists ship to the same address:
{
"shippingAddress": {
"name": "John Doe",
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
}
}
Per-Artist Addresses
When shippingOption: "per_artist", each artist order has its own address:
{
"shippingOption": "per_artist",
"shippingAddress": null,
"artistOrders": [
{
"artistName": "Jane Smith",
"shippingAddress": {
"name": "Alice (Birthday Gift)",
"line1": "456 Oak Ave",
"city": "Boston",
"state": "MA",
"postal_code": "02101",
"country": "US"
}
},
{
"artistName": "Bob Johnson",
"shippingAddress": {
"name": "Carol (Holiday Gift)",
"line1": "789 Pine St",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US"
}
}
]
}
Timeline & Expectations
Typical Order Timeline
| Status | Timeline | What's Happening |
|---|---|---|
paid | Immediately after checkout | Payment processed, artists notified |
processing | 1-3 days | Artists preparing and packing items |
partially_shipped | 2-7 days | First artist(s) shipped |
shipped | 3-10 days | All artists shipped |
delivered | 5-14 days | All packages delivered |
Why Different Arrival Times?
- Independent artists: Each artist manages their own fulfillment
- Different locations: Artists ship from different locations
- Different carriers: Artists choose their preferred carriers
- Different processing times: Some artists ship same-day, others take 2-3 days
This is intentional - buyers are supporting independent artists who maintain control of their businesses.
Use Cases
Customer Order Tracking Page
import { useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
export default function OrderTrackingPage() {
const { publicId } = useParams();
const [order, setOrder] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchOrder() {
const res = await fetch(`/api/gallery/orders/${publicId}`);
const data = await res.json();
setOrder(data.order);
setLoading(false);
}
fetchOrder();
}, [publicId]);
if (loading) return <div>Loading order...</div>;
if (!order) return <div>Order not found</div>;
return (
<div>
<h1>Order {order.publicId}</h1>
<p>Status: {order.status}</p>
<p>Total: ${order.total}</p>
<h2>Shipments</h2>
{order.artistOrders.map(ao => (
<div key={ao.id}>
<h3>{ao.artistName}</h3>
<p>Status: {ao.status}</p>
{ao.trackingNumber && (
<a href={ao.trackingUrl} target="_blank">
Track: {ao.trackingNumber}
</a>
)}
<ul>
{ao.items.map(item => (
<li key={item.id}>
{item.quantity}x {item.productName} @ ${item.price}
</li>
))}
</ul>
</div>
))}
</div>
);
}
Email Notification Link
Order confirmation emails include a tracking link:
https://gallery.artbase.studio/gallery/orders/GAL-2024-ABC123
Customers can bookmark this URL to track their order anytime.
Security & Privacy
Public ID Design
Order IDs use a public format (GAL-YYYY-XXXXXX) that:
- ✅ Is safe to expose in URLs
- ✅ Is difficult to guess (random 6-character suffix)
- ✅ Doesn't reveal internal database IDs
- ✅ Includes year for easy human reference
What's Exposed
The order endpoint intentionally exposes:
- ✅ Order status and shipment info
- ✅ Product names and images
- ✅ Shipping address (customer's own)
- ✅ Total and pricing breakdown
- ❌ Payment method details
- ❌ Internal UUIDs
- ❌ Stripe IDs
Guessing Protection
While public order IDs are accessible without auth, they're difficult to guess:
- Format:
GAL-{YEAR}-{6-char random} - Charset: Alphanumeric uppercase (36^6 = 2.1 billion combinations per year)
- Example:
GAL-2024-H7K9M2