Enterprise

API & SDK Documentation

Integrate PackProof into your systems with our REST API and TypeScript SDK. Automate photo uploads, certifications, and real-time event notifications.

Base URL: https://app.packproof.com/api/v1

Getting Started

The PackProof API lets you programmatically manage orders, upload photos, create certifications, and receive real-time webhook notifications. Available on the Enterprise plan.

1. Get your API key

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.

2. Make your first request

curl https://app.packproof.com/api/v1/organization \
  -H "X-API-Key: pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"

3. Or use the SDK

npm install @packproof/sdk
import { PackProof } from '@packproof/sdk';

const client = new PackProof({
  apiKey: process.env.PACKPROOF_API_KEY,
});

const org = await client.organization.get();
console.log(org.name);

Authentication

Include your API key in the X-API-Key header on every request.

X-API-Key: pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4

API 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.

Response format

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" } }

Organization

GET
/organization

Get 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

Orders represent jobs, shipments, or work units that contain photos.

GET
/orders

List orders (paginated, filterable)

GET
/orders/:id

Get a single order

POST
/orders

Create a new order

List orders

ParameterTypeRequiredDescription
statusstringNoFilter by "active" or "completed"
searchstringNoSearch order names
limitintegerNoResults per page (max 100, default 20)
offsetintegerNoPagination 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 }
}

Create order

ParameterTypeRequiredDescription
namestringYesName for the order
clientIdstringNoAssociate 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

Photos are media files (images and videos) attached to orders.

GET
/photos

List photos (filterable by orderId)

GET
/photos/:id

Get photo metadata

POST
/photos

Upload a photo (multipart/form-data)

DELETE
/photos/:id

Delete a photo permanently

Upload a photo

Use multipart/form-data. Max file size: 50 MB.

ParameterTypeRequiredDescription
filebinaryYesThe image or video file
orderIdstringYesOrder to attach the photo to
fileNamestringNoCustom 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"

Photo response

{
  "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

Clients represent repeat customers or business entities that place orders.

GET
/clients

List all clients

GET
/clients/:id

Get a single client

POST
/clients

Create a new client

PATCH
/clients/:id

Update a client

Create client

ParameterTypeRequiredDescription
namestringYesClient name (must be unique within org)
notesstringNoInternal 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

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.

GET
/certifications

List all certifications

GET
/certifications/:certId

Get a certification by ID

POST
/certifications

Certify a photo

POST
/certifications/batch

Batch certify multiple photos

GET
/certifications/verify/:certId(no auth)

Public verification (anyone can verify)

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"}'
// Response (201 Created)
{
  "data": {
    "certificationId": "ATT-2025-A1B2C3D4",
    "photoId": "photo_abc",
    "certifiedAt": "2025-03-10T15:00:00.000Z",
    "contentHash": "e3b0c44298fc1c149afbf4c8...",
    "signature": "..."
  }
}

Batch certify

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 }
    ]
  }
}

Verify a certification (public)

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"
    }
  }
}

Webhooks

Receive real-time HTTP POST notifications when events occur. Register a URL, subscribe to events, and PackProof will send signed payloads to your server.

GET
/webhooks

List webhooks

POST
/webhooks

Create a webhook (returns signing secret)

GET
/webhooks/:id

Get a webhook

PATCH
/webhooks/:id

Update a webhook

DELETE
/webhooks/:id

Delete a webhook

GET
/webhooks/:id/deliveries

View delivery history

POST
/webhooks/:id/test

Send a test event

Event types

EventTrigger
photo.uploadedA photo is uploaded (UI or API)
photo.deletedA photo is deleted (UI or API)
certification.createdA photo is certified
order.createdA new order is created
order.completedAn order is archived/completed

Create a webhook

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.

Verifying signatures

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');
}

Webhook headers

HeaderDescription
X-PackProof-Signaturesha256=<HMAC-SHA256 hex digest>
X-PackProof-EventEvent type (e.g., photo.uploaded)
X-PackProof-Delivery-IDUnique delivery ID for deduplication

Retry behavior

If your server doesn't return 2xx within 10 seconds, PackProof retries:

AttemptDelayTotal Elapsed
1Immediate0s
21 minute~1 min
310 minutes~11 min

TypeScript SDK

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.

Installation

npm install @packproof/sdk
# or
yarn add @packproof/sdk
# or
pnpm add @packproof/sdk

Configuration

import { 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)
});

Available resources

ResourceMethods
client.organizationget()
client.orderslist(), get(), create()
client.photoslist(), get(), upload(), delete()
client.clientslist(), get(), create(), update()
client.certificationslist(), get(), create(), batch(), verify()
client.webhookslist(), get(), create(), update(), delete(), deliveries(), test()

Usage examples

// 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!

Error handling

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.

Pagination

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;
}

Error Handling

All API errors follow the same structure:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Order name is required"
  }
}

Error codes

CodeHTTPDescription
UNAUTHORIZED401Missing or invalid API key
FORBIDDEN403Valid key but lacks access (e.g., non-Enterprise plan)
NOT_FOUND404Resource does not exist or belongs to another org
VALIDATION_ERROR400Missing required fields or invalid data
CONFLICT409Resource already exists (e.g., duplicate certification)
RATE_LIMIT_EXCEEDED429Too many requests (1,000/hour per key)
INTERNAL_ERROR500Server error (safe to retry)

Retry strategy

ErrorRetry?Strategy
401/403/404/400/409NoFix the request
429 Rate LimitedYesWait for X-RateLimit-Reset, then retry
500 Internal ErrorYesRetry with exponential backoff
Network errorYesRetry with exponential backoff

Rate Limiting

Every API response includes rate limit headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1710100800
HeaderDescription
X-RateLimit-LimitMaximum requests per 1-hour window (1,000)
X-RateLimit-RemainingRemaining requests in the current window
X-RateLimit-ResetUnix 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.

Examples

Sync orders to your CRM

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();

Upload and certify photos

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;
}

Webhook server (Express)

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);

Python integration

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']}")

cURL quick reference

# 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