Enterprise

API & SDK Documentation

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

Base URL: https://app.packproof.com/api/v1OpenAPI Spec →

Getting Started

The PackProof API lets you programmatically manage orders, upload media, seal evidence, 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);

4. Interactive API docs

A Swagger UI explorer is available at /api/v1/docs. The OpenAPI 3.0 spec can be fetched at /api/v1/docs/openapi.json.

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 media items.

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

Media

Media items are photos and videos attached to orders. The API accepts both image and video uploads through the same endpoint.

GET
/media

List media items (filterable by orderId)

GET
/media/:id

Get media item metadata

POST
/media

Upload a media file (multipart/form-data)

DELETE
/media/:id

Delete a media item permanently

Note: Photos and videos are unified under a single /media resource. The server detects which is which from the upload's MIME type; on response objects, image-specific fields like metadata.file.width/.height are present only for images.

Supported media types

CategoryFormatsMIME Types
ImagesJPEG, PNG, WebP, HEICimage/jpeg, image/png, image/webp, image/heic
VideosMP4, WebM, MOV, M4Vvideo/mp4, video/webm, video/quicktime, video/x-m4v

Maximum file size: 50 MB for API uploads. Other PackProof workflows (camera capture, file picker) support larger uploads via chunked transfer.

Upload media

Use multipart/form-data. The server detects whether the file is a photo or video from the MIME type and file extension.

ParameterTypeRequiredDescription
filebinaryYesThe image or video file
orderIdstringYesOrder to attach the media to
fileNamestringNoCustom filename (auto-generated if omitted)

Photo upload

curl -X POST https://app.packproof.com/api/v1/media \
  -H "X-API-Key: pk_live_..." \
  -F "file=@/path/to/photo.jpg" \
  -F "orderId=order_789"

Video upload

curl -X POST https://app.packproof.com/api/v1/media \
  -H "X-API-Key: pk_live_..." \
  -F "file=@/path/to/inspection.mp4" \
  -F "orderId=order_789"

When fileName is omitted, the server generates a name in the format photo-20250310-143000-a1b2c3d4.jpg or video-20250310-143000-a1b2c3d4.mp4.

Media response

// API upload response (image)
{
  "data": {
    "id": "media_abc",
    "orderId": "order_789",
    "organizationId": "org_abc123",
    "fileName": "photo-20250310-143000-a1b2c3d4.jpg",
    "fullUrl": "https://...",
    "thumbnailUrl": "https://...",
    "posterFileId": null,
    "metadata": {
      "timestamp": "2025-03-10T14:30:00.000Z",
      "file": {
        "size": 2048576,
        "width": 4032,
        "height": 3024,
        "mimeType": "image/jpeg"
      },
      "device": { "ipAddress": "api" }
    },
    "uploadedAt": "2025-03-10T14:30:05.000Z"
  }
}

Media captured through the PackProof app may also include metadata.location (GPS coordinates) and device details. These fields are not present for API uploads.

posterFileId is always null for API uploads. Poster thumbnails are only generated for media uploaded through the PackProof UI. metadata.file.width and metadata.file.height are populated for images only. metadata.location is present only when the capturing device provides GPS coordinates (typically device-captured media, not API uploads).

Delete media

curl -X DELETE https://app.packproof.com/api/v1/media/media_abc \
  -H "X-API-Key: pk_live_..."
// Response
{ "data": { "id": "media_abc", "deleted": true } }

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

Sealing & Verification

Sealing provides cryptographic proof that a media item existed in its exact form at a specific point in time. Each seal includes SHA-512 content hashes, an HMAC-SHA-512 digital signature, and a unique certificate ID.

GET
/certifications

List all seals

GET
/certifications/:certId

Get a seal by certificate ID

POST
/certifications

Seal a media item

POST
/certifications/batch

Batch seal multiple media items

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

Public verification (anyone can verify)

Note: API paths use /certifications for backward compatibility. The user-facing term is "sealing."

Seal a media item

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-A1B2C3D4E5F6A7B8",
    "photoId": "photo_abc",
    "organizationId": "org_abc123",
    "certifiedBy": "api",
    "certifiedAt": "2025-03-10T15:00:00.000Z",
    "contentHash": "e3b0c44298fc1c149afbf4c8996fb924...",
    "metadataHash": "9f86d081884c7d659a2feaa0c55ad015a3...",
    "signature": "a7ffc6f8bf1ed76651c14756a061d662...",
    "photoMetadataSnapshot": {
      "fileName": "photo-20250310-143000-a1b2c3d4.jpg",
      "capturedAt": "2025-03-10T14:30:00.000Z",
      "location": { "latitude": -33.8688, "longitude": 151.2093 },
      "device": { "platform": "api" }
    },
    "verificationCount": 0
  }
}

Sealing a media item that is already sealed returns 409 Conflict. Certificate IDs follow the format ATT-YYYY-XXXXXXXXXXXXXXXX (16 uppercase hex characters).

Batch seal

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-A1B2C3D4E5F6A7B8", "success": true },
      { "photoId": "photo_2", "certificationId": "ATT-2025-E5F6A7B8C9D0E1F2", "success": true },
      { "photoId": "photo_3", "certificationId": "ATT-2025-C9D0E1F2A3B4C5D6", "success": true }
    ]
  }
}

Items that are already sealed are included in results with their existing certificate ID and success: true. Items are processed in parallel batches of 5.

Verify a seal (public)

No authentication required. Anyone with the certificate ID can verify that the evidence has not been altered.

curl https://app.packproof.com/api/v1/certifications/verify/ATT-2025-A1B2C3D4E5F6A7B8
{
  "data": {
    "verified": true,
    "certificationId": "ATT-2025-A1B2C3D4E5F6A7B8",
    "certifiedAt": "2025-03-10T15:00:00.000Z",
    "organization": "Acme Logistics",
    "signatureValid": true,
    "contentHash": "e3b0c44298fc1c149afbf4c8996fb924...",
    "verificationCount": 4,
    "photoMetadata": {
      "fileName": "photo-20250310-143000-a1b2c3d4.jpg",
      "capturedAt": "2025-03-10T14:30:00.000Z",
      "location": {
        "latitude": -33.8688,
        "longitude": 151.2093,
        "address": "Sydney Harbour"
      },
      "device": {
        "platform": "iPhone",
        "model": "iPhone 15 Pro",
        "osName": "iOS",
        "osVersion": "17.4"
      },
      "timezone": "AEST"
    }
  }
}

Verification response fields

FieldDescription
verifiedWhether the HMAC-SHA-512 signature is valid
signatureValidSame as verified (explicit signature check result)
contentHashSHA-512 hash of the original file bytes at time of sealing
verificationCountNumber of times this certificate has been verified
photoMetadataFrozen snapshot of metadata captured at time of sealing

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
media.uploadedA media item is uploaded (UI or API)
media.deletedA media item is deleted (UI or API)
certification.createdA media item is sealed
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": ["media.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.

Webhook update

Update a webhook's URL, events, description, or active status. All fields are optional.

ParameterTypeRequiredDescription
urlstringNoNew webhook URL
eventsstring[]NoUpdated event subscriptions
descriptionstringNoUpdated description
isActivebooleanNoEnable or disable the webhook

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

// Upload a video
const video = await client.photos.upload({
  orderId: 'order_123',
  file: fs.readFileSync('/path/to/inspection.mp4'),
});

// Seal a media item
const cert = await client.certifications.create({
  photoId: photo.id,
});
console.log(cert.certificationId); // "ATT-2025-A1B2C3D4E5F6A7B8"

// Batch seal
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-A1B2C3D4E5F6A7B8');

// Create a webhook
const webhook = await client.webhooks.create({
  url: 'https://your-server.com/webhooks',
  events: ['media.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., media already sealed)
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 seal evidence

import { PackProof } from '@packproof/sdk';
import fs from 'fs';

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

async function uploadAndSeal(orderId, files) {
  const photoIds = [];

  // Upload all media
  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 seal
  const result = await client.certifications.batch({ photoIds });
  console.log(`Sealed: ${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 'media.uploaded':
        // Handle new media upload...
        break;
      case 'certification.created':
        // Handle sealed evidence...
        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']} items")

# Upload a photo
with open('inspection.jpg', 'rb') as f:
    response = requests.post(
        f'{BASE_URL}/media',
        headers=HEADERS,
        files={'file': ('inspection.jpg', f, 'image/jpeg')},
        data={'orderId': orders[0]['id']}
    )
    photo = response.json()['data']

# Upload a video
with open('walkthrough.mp4', 'rb') as f:
    response = requests.post(
        f'{BASE_URL}/media',
        headers=HEADERS,
        files={'file': ('walkthrough.mp4', f, 'video/mp4')},
        data={'orderId': orders[0]['id']}
    )
    video = response.json()['data']

# Seal evidence
response = requests.post(
    f'{BASE_URL}/certifications',
    headers=HEADERS,
    json={'photoId': photo['id']}
)
cert = response.json()['data']
print(f"Sealed: {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/media \
  -H "X-API-Key: pk_live_..." \
  -F "file=@/path/to/photo.jpg" \
  -F "orderId=order_123"

# Upload a video
curl -X POST https://app.packproof.com/api/v1/media \
  -H "X-API-Key: pk_live_..." \
  -F "file=@/path/to/inspection.mp4" \
  -F "orderId=order_123"

# Seal evidence
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-A1B2C3D4E5F6A7B8