Integrate PackProof into your systems with our REST API and TypeScript SDK. Automate photo uploads, certifications, and real-time event notifications.
The PackProof API lets you programmatically manage orders, upload photos, create certifications, and receive real-time webhook notifications. Available on the Enterprise plan.
Navigate to Settings → API Keys in your PackProof dashboard. Click Create API Key, give it a name, and copy the key. The full key is shown only once.
curl https://app.packproof.com/api/v1/organization \
-H "X-API-Key: pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"npm install @packproof/sdkimport { PackProof } from '@packproof/sdk';
const client = new PackProof({
apiKey: process.env.PACKPROOF_API_KEY,
});
const org = await client.organization.get();
console.log(org.name);Include your API key in the X-API-Key header on every request.
X-API-Key: pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4API keys are scoped to a single organization. Each key is hashed with SHA-256 before storage — PackProof never stores your raw key. If you lose it, revoke and create a new one.
All responses use a consistent envelope:
// Single resource
{ "data": { "id": "order_123", ... } }
// List
{ "data": [...], "pagination": { "total": 150, "limit": 20, "offset": 0, "hasMore": true } }
// Error
{ "error": { "code": "VALIDATION_ERROR", "message": "Order name is required" } }/organizationGet organization info for the authenticated API key
// Response
{
"data": {
"id": "org_abc123",
"name": "Acme Logistics",
"slug": "acme-logistics",
"industry": "Maritime",
"planTier": "enterprise",
"createdAt": "2025-01-15T10:30:00.000Z"
}
}Orders represent jobs, shipments, or work units that contain photos.
/ordersList orders (paginated, filterable)
/orders/:idGet a single order
/ordersCreate a new order
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by "active" or "completed" |
search | string | No | Search order names |
limit | integer | No | Results per page (max 100, default 20) |
offset | integer | No | Pagination offset (default 0) |
// Response
{
"data": [
{
"id": "order_123",
"orderNumber": "Ship Alpha",
"status": "in-progress",
"photoCount": 24,
"createdAt": "2025-03-01T09:15:00.000Z"
}
],
"pagination": { "total": 45, "limit": 20, "offset": 0, "hasMore": true }
}| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Name for the order |
clientId | string | No | Associate with an existing client |
curl -X POST https://app.packproof.com/api/v1/orders \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Container Ship Beta", "clientId": "client_456"}'Photos are media files (images and videos) attached to orders.
/photosList photos (filterable by orderId)
/photos/:idGet photo metadata
/photosUpload a photo (multipart/form-data)
/photos/:idDelete a photo permanently
Use multipart/form-data. Max file size: 50 MB.
| Parameter | Type | Required | Description |
|---|---|---|---|
file | binary | Yes | The image or video file |
orderId | string | Yes | Order to attach the photo to |
fileName | string | No | Custom filename (auto-generated if omitted) |
curl -X POST https://app.packproof.com/api/v1/photos \
-H "X-API-Key: pk_live_..." \
-F "file=@/path/to/photo.jpg" \
-F "orderId=order_789"{
"data": {
"id": "photo_abc",
"fileName": "api-1710000000-image.jpg",
"fullUrl": "https://...",
"thumbnailUrl": "https://...",
"metadata": {
"timestamp": "2025-03-10T14:30:00.000Z",
"location": {
"latitude": -33.8688,
"longitude": 151.2093,
"address": "Sydney Harbour"
},
"file": { "size": 2048576, "width": 4032, "height": 3024, "mimeType": "image/jpeg" }
},
"uploadedAt": "2025-03-10T14:30:05.000Z"
}
}Clients represent repeat customers or business entities that place orders.
/clientsList all clients
/clients/:idGet a single client
/clientsCreate a new client
/clients/:idUpdate a client
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Client name (must be unique within org) |
notes | string | No | Internal notes (max 2000 chars) |
curl -X POST https://app.packproof.com/api/v1/clients \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Pacific Freight Co.", "notes": "Primary contact: John Smith"}'Certifications provide cryptographic proof that a photo existed in its exact form at a specific point in time. Each includes SHA-512 content hashes and a digital signature.
/certificationsList all certifications
/certifications/:certIdGet a certification by ID
/certificationsCertify a photo
/certifications/batchBatch certify multiple photos
/certifications/verify/:certId(no auth)Public verification (anyone can verify)
curl -X POST https://app.packproof.com/api/v1/certifications \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"photoId": "photo_abc"}'// Response (201 Created)
{
"data": {
"certificationId": "ATT-2025-A1B2C3D4",
"photoId": "photo_abc",
"certifiedAt": "2025-03-10T15:00:00.000Z",
"contentHash": "e3b0c44298fc1c149afbf4c8...",
"signature": "..."
}
}curl -X POST https://app.packproof.com/api/v1/certifications/batch \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"photoIds": ["photo_1", "photo_2", "photo_3"]}'// Response
{
"data": {
"certified": 2,
"alreadyCertified": 1,
"total": 3,
"results": [
{ "photoId": "photo_1", "certificationId": "ATT-2025-AAAA1111", "success": true },
{ "photoId": "photo_2", "certificationId": "ATT-2025-BBBB2222", "success": true },
{ "photoId": "photo_3", "certificationId": "ATT-2025-CCCC3333", "success": true }
]
}
}No authentication required. Anyone with the certification ID can verify.
curl https://app.packproof.com/api/v1/certifications/verify/ATT-2025-A1B2C3D4{
"data": {
"verified": true,
"certificationId": "ATT-2025-A1B2C3D4",
"certifiedAt": "2025-03-10T15:00:00.000Z",
"organization": "Acme Logistics",
"signatureValid": true,
"verificationCount": 4,
"photoMetadata": {
"fileName": "photo.jpg",
"capturedAt": "2025-03-10T14:30:00.000Z"
}
}
}Receive real-time HTTP POST notifications when events occur. Register a URL, subscribe to events, and PackProof will send signed payloads to your server.
/webhooksList webhooks
/webhooksCreate a webhook (returns signing secret)
/webhooks/:idGet a webhook
/webhooks/:idUpdate a webhook
/webhooks/:idDelete a webhook
/webhooks/:id/deliveriesView delivery history
/webhooks/:id/testSend a test event
| Event | Trigger |
|---|---|
photo.uploaded | A photo is uploaded (UI or API) |
photo.deleted | A photo is deleted (UI or API) |
certification.created | A photo is certified |
order.created | A new order is created |
order.completed | An order is archived/completed |
curl -X POST https://app.packproof.com/api/v1/webhooks \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/packproof",
"events": ["photo.uploaded", "certification.created"],
"description": "Warehouse sync"
}'Important: The signing secret is returned only in the creation response. Store it securely. If you lose it, delete the webhook and create a new one.
Every delivery includes an X-PackProof-Signature header. Verify it using HMAC-SHA256:
import crypto from 'crypto';
// Verify signature
const signature = req.headers['x-packproof-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.PACKPROOF_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}| Header | Description |
|---|---|
X-PackProof-Signature | sha256=<HMAC-SHA256 hex digest> |
X-PackProof-Event | Event type (e.g., photo.uploaded) |
X-PackProof-Delivery-ID | Unique delivery ID for deduplication |
If your server doesn't return 2xx within 10 seconds, PackProof retries:
| Attempt | Delay | Total Elapsed |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1 minute | ~1 min |
| 3 | 10 minutes | ~11 min |
The @packproof/sdk package provides a type-safe client for the PackProof API. Works with Node.js 18+, Deno, Bun, and any runtime with native fetch.
npm install @packproof/sdk
# or
yarn add @packproof/sdk
# or
pnpm add @packproof/sdkimport { PackProof } from '@packproof/sdk';
const client = new PackProof({
apiKey: process.env.PACKPROOF_API_KEY, // Required
baseUrl: 'https://app.packproof.com', // Optional (default)
maxRetries: 3, // Optional (default: 3)
});| Resource | Methods |
|---|---|
client.organization | get() |
client.orders | list(), get(), create() |
client.photos | list(), get(), upload(), delete() |
client.clients | list(), get(), create(), update() |
client.certifications | list(), get(), create(), batch(), verify() |
client.webhooks | list(), get(), create(), update(), delete(), deliveries(), test() |
// List active orders
const { data: orders, pagination } = await client.orders.list({
status: 'active',
limit: 50,
});
// Upload a photo
import fs from 'fs';
const photo = await client.photos.upload({
orderId: 'order_123',
file: fs.readFileSync('/path/to/photo.jpg'),
fileName: 'inspection-001.jpg',
});
// Certify a photo
const cert = await client.certifications.create({
photoId: photo.id,
});
console.log(cert.certificationId); // "ATT-2025-A1B2C3D4"
// Batch certify
const result = await client.certifications.batch({
photoIds: ['photo_1', 'photo_2', 'photo_3'],
});
// Verify (public, no auth needed)
const verification = await client.certifications.verify('ATT-2025-A1B2C3D4');
// Create a webhook
const webhook = await client.webhooks.create({
url: 'https://your-server.com/webhooks',
events: ['photo.uploaded', 'certification.created'],
});
console.log(webhook.secret); // Save this!import { PackProof, PackProofError } from '@packproof/sdk';
try {
await client.photos.get('nonexistent');
} catch (err) {
if (err instanceof PackProofError) {
console.error(`API Error [${err.code}]: ${err.message}`);
console.error(`Status: ${err.statusCode}`);
}
}The SDK automatically retries 5xx errors with exponential backoff (500ms, 1s, 2s). Client errors (4xx) are never retried.
async function getAllOrders(client) {
const allOrders = [];
let offset = 0;
const limit = 100;
while (true) {
const { data, pagination } = await client.orders.list({ limit, offset });
allOrders.push(...data);
if (!pagination.hasMore) break;
offset += limit;
}
return allOrders;
}All API errors follow the same structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Order name is required"
}
}| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API key |
FORBIDDEN | 403 | Valid key but lacks access (e.g., non-Enterprise plan) |
NOT_FOUND | 404 | Resource does not exist or belongs to another org |
VALIDATION_ERROR | 400 | Missing required fields or invalid data |
CONFLICT | 409 | Resource already exists (e.g., duplicate certification) |
RATE_LIMIT_EXCEEDED | 429 | Too many requests (1,000/hour per key) |
INTERNAL_ERROR | 500 | Server error (safe to retry) |
| Error | Retry? | Strategy |
|---|---|---|
| 401/403/404/400/409 | No | Fix the request |
| 429 Rate Limited | Yes | Wait for X-RateLimit-Reset, then retry |
| 500 Internal Error | Yes | Retry with exponential backoff |
| Network error | Yes | Retry with exponential backoff |
Every API response includes rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1710100800| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per 1-hour window (1,000) |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When the limit is exceeded, the API returns 429 with the RATE_LIMIT_EXCEEDED error code. Check the X-RateLimit-Reset header and wait before retrying.
import { PackProof } from '@packproof/sdk';
const client = new PackProof({ apiKey: process.env.PACKPROOF_API_KEY });
async function syncOrdersToCRM() {
let offset = 0;
const limit = 100;
while (true) {
const { data: orders, pagination } = await client.orders.list({ limit, offset });
for (const order of orders) {
await upsertOrderInCRM({
externalId: order.id,
name: order.orderNumber,
status: order.status,
photoCount: order.photoCount,
});
}
if (!pagination.hasMore) break;
offset += limit;
}
}
// Run every 15 minutes via cron
syncOrdersToCRM();import { PackProof } from '@packproof/sdk';
import fs from 'fs';
const client = new PackProof({ apiKey: process.env.PACKPROOF_API_KEY });
async function uploadAndCertify(orderId, files) {
const photoIds = [];
// Upload all photos
for (const file of files) {
const photo = await client.photos.upload({
orderId,
file: fs.readFileSync(file.path),
fileName: file.name,
});
photoIds.push(photo.id);
console.log(`Uploaded: ${photo.id}`);
}
// Batch certify
const result = await client.certifications.batch({ photoIds });
console.log(`Certified: ${result.certified}/${result.total}`);
return result;
}import express from 'express';
import crypto from 'crypto';
const app = express();
app.post('/webhooks/packproof',
express.raw({ type: 'application/json' }),
(req, res) => {
// Verify signature
const signature = req.headers['x-packproof-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.PACKPROOF_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body.toString());
switch (payload.event) {
case 'photo.uploaded':
// Handle new photo...
break;
case 'certification.created':
// Handle certification...
break;
case 'order.completed':
// Handle completion...
break;
}
res.status(200).send('OK');
}
);
app.listen(3000);import requests
import os
BASE_URL = 'https://app.packproof.com/api/v1'
HEADERS = {'X-API-Key': os.environ['PACKPROOF_API_KEY']}
# List orders
response = requests.get(f'{BASE_URL}/orders', headers=HEADERS, params={'status': 'active'})
orders = response.json()['data']
for order in orders:
print(f"{order['orderNumber']}: {order['photoCount']} photos")
# Upload a photo
with open('inspection.jpg', 'rb') as f:
response = requests.post(
f'{BASE_URL}/photos',
headers=HEADERS,
files={'file': ('inspection.jpg', f, 'image/jpeg')},
data={'orderId': orders[0]['id']}
)
photo = response.json()['data']
# Certify
response = requests.post(
f'{BASE_URL}/certifications',
headers=HEADERS,
json={'photoId': photo['id']}
)
cert = response.json()['data']
print(f"Certified: {cert['certificationId']}")# List active orders
curl -s https://app.packproof.com/api/v1/orders?status=active \
-H "X-API-Key: pk_live_..."
# Upload a photo
curl -X POST https://app.packproof.com/api/v1/photos \
-H "X-API-Key: pk_live_..." \
-F "file=@/path/to/photo.jpg" \
-F "orderId=order_123"
# Certify a photo
curl -X POST https://app.packproof.com/api/v1/certifications \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"photoId": "photo_abc"}'
# Verify (no auth needed)
curl https://app.packproof.com/api/v1/certifications/verify/ATT-2025-A1B2C3D4