E-Commerce Integration Guide

Connect your platform to PCC for seamless labor law poster fulfillment

API Base URL

All requests go to:

https://api.postercompliance.com

DROPSHIP Quick Start for Dropship Partners

As a dropship partner, you send orders and we ship directly to your customers under your contracted rates. Here's your simplified flow:

1. Get Products GET /v1/products
2. Submit Order POST /v1/checkout
3. Get Tracking GET /v1/orders/.../tracking
How Dropship Works:
  • You send orders - Submit customer details and products via API
  • We handle pricing - Send $0 for prices, we apply your contracted rates
  • We calculate tax - Send $0 for tax, we calculate based on ship-to address
  • We ship to your customer - Direct fulfillment from our warehouse
  • We invoice you - Consolidated billing per your agreement

Complete Dropship Checkout Example

Here's exactly what your checkout request should look like:

POST /v1/checkout HTTP/1.1
Host: api.postercompliance.com
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

{
  "account": {
    "name": "John Smith",
    "email": "john.smith@example.com",
    "phone": "555-123-4567",
    "shippingAddress": {
      "line1": "123 Main Street",
      "line2": "Suite 100",
      "city": "Austin",
      "stateOrProvince": "TX",
      "postalCode": "78701",
      "country": "US"
    }
  },
  "order": {
    "externalOrderNumber": "YOUR-ORDER-12345",
    "orderType": "New",
    "lineItems": [
      {
        "sku": "1-P-SP-TX-XX-A-E-L-0325",
        "quantity": 1,
        "pricePerUnit": 0
      }
    ],
    "shippingAmount": 0,
    "taxAmount": 0,
    "payment": {
      "transactionId": "YOUR-ORDER-12345",
      "paymentDate": "2026-02-15T10:00:00Z",
      "amount": 0
    }
  }
}

Note: All pricing fields are $0 because PCC applies your contracted rates automatically. Use "orderType": "Renewal" with originalOrderNumber for subscription renewals. Add "testMode": true to test without creating real orders.

Full Integration Guide

1

Authentication

PCC will provide you with an API token. Include it in all requests:

Authorization: Bearer YOUR_API_TOKEN

Your token determines:

2

Get Products

Retrieve our product catalog. Cache this data and refresh daily or weekly.

GET /v1/products

Request:

GET /v1/products HTTP/1.1
Host: api.postercompliance.com
Authorization: Bearer YOUR_API_TOKEN

Response:

{
  "success": true,
  "message": "Successfully retrieved 150 available products (price source: Default)",
  "data": {
    "products": [
      {
        "name": "California State & Federal Labor Law Poster - 1 Year Plan - AIO - English",
        "productNumber": "1-P-SP-CA-XX-A-E-L-0325",
        "price": 149.99,
        "longDescription": "Comprehensive California state and federal labor law poster with all required postings, updated automatically for 1 year.",
        "photoUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
        "details": {
          "posterType": "SP",
          "posterTypeDescription": "State Poster",
          "state": "CA",
          "stateName": "California",
          "cityCounty": "XX",
          "cityCountyDescription": "California Only",
          "language": "English",
          "languageDescription": "English",
          "isStateFederal": true,
          "isCityCounty": false,
          "planTerm": 1,
          "productType": "Plan",
          "productTypeDescription": "1 Yr Plan",
          "posterFormat": "AllInOne",
          "posterFormatDescription": "All In One"
        }
      }
    ],
    "totalCount": 150,
    "priceLevelApplied": null,
    "pricesOverridden": 0
  }
}

Key Product Fields

Field Description
productNumber Use this when placing orders. Unique product SKU identifier.
price Retail price. Dropship partners: your contracted rate may differ.
details.productType "Plan" (annual subscription with updates) or "Set" (one-time purchase)
details.posterFormat "AllInOne" (single poster) or "SeparatePosters" (multiple posters)
details.state 2-letter state code (e.g., "CA", "TX", "NY")
photoUrl Product image as an inline data:image/...;base64, URI, or null. Usable directly as an <img src>; it is not a fetchable HTTP link, so don't store it expecting a short URL.
longDescription Extended product description from Dynamics (can be null)
Filter by type: Add ?productType=Plan for subscriptions only, or ?productType=Set for one-time purchases.
Important: Use the productNumber value as the sku field when placing checkout orders.

Shipping Prices (Optional)

Standard integrations that charge for shipping can look up shipping-method prices at your account's price level. Dropship partners who ship at $0 don't need this.

GET /v1/products/shipping?accountNumber=1234567

Response:

{
  "success": true,
  "message": "Successfully retrieved shipping prices (source: Account)",
  "data": {
    "shippingMethods": [
      {
        "method": "Standard",
        "name": "Shipping Standard",
        "productNumber": "N-S-SH-SD-XX-X-X-X-0000",
        "price": 9.95,
        "isDefault": true
      }
    ],
    "priceLevelSource": "Account",
    "accountNumber": "1234567"
  }
}
Price source: accountNumber is optional and must be one of your own accounts. With it, prices come from that account's price level (priceLevelSource: "Account"); without it, your token's default price level or the system default is used.
3

Calculate Tax (Optional for Dropship)

Dropship Partners: You can skip this step. When you submit an order with $0 for tax, we calculate it automatically based on the shipping address.

For standard integrations where you collect payment, get the tax estimate first:

POST /v1/orders/estimate
POST /v1/orders/estimate HTTP/1.1
Host: api.postercompliance.com
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

{
  "shippingDestinations": [
    {
      "destinationId": "1",
      "shippingAddress": {
        "street": "123 Main Street",
        "city": "Austin",
        "state": "TX",
        "zipCode": "78701"
      },
      "items": [
        {
          "productNumber": "1-P-SP-TX-XX-A-E-L-0325",
          "quantity": 1,
          "unitPrice": 149.99
        }
      ],
      "shippingAmount": 14.99
    }
  ]
}

Response:

{
  "success": true,
  "message": "Successfully calculated estimate for 1 destinations",
  "data": {
    "orderId": null,
    "destinationResults": [
      {
        "destinationId": "1",
        "shippingAddress": {
          "street": "123 Main Street",
          "city": "Austin",
          "state": "TX",
          "zipCode": "78701",
          "country": "US"
        },
        "subtotal": 149.99,
        "shippingAmount": 14.99,
        "taxAmount": 13.61,
        "total": 178.59,
        "lineItemTaxes": [
          {
            "lineId": null,
            "productNumber": "1-P-SP-TX-XX-A-E-L-0325",
            "quantity": 1,
            "unitPrice": 149.99,
            "extendedPrice": 149.99,
            "taxAmount": 12.37,
            "totalWithTax": 162.36
          }
        ],
        "taxCloudCartId": "abc123-def456",
        "isExempt": false,
        "taxCalculationSuccess": true,
        "taxCalculationError": null
      }
    ],
    "totalTax": 13.61,
    "totalSubtotal": 149.99,
    "totalShipping": 14.99,
    "grandTotal": 178.59,
    "allTaxCalculated": true,
    "taxCalculationWarning": null
  }
}
Important: Check taxCalculationSuccess. If false, the tax couldn't be calculated - don't proceed to checkout. Note that taxAmount covers products and shipping, while lineItemTaxes lists the product lines only - so those line taxes can sum to less than taxAmount.
4

Submit Order

Submit the order for fulfillment. Choose the tab that matches your scenario.

Scopes at a glance: ProductsRead for the product endpoints in this step, OrdersRead for estimates, validation, tracking, and job polling, OrdersCreate for checkout. Ask for all three when your token is provisioned.
Validate first (optional): before going live, POST the same checkout body to /v1/checkout/validate to check fields, SKUs, and the account without creating anything. Like every endpoint, the result arrives inside the standard response envelope: { "success": true, "data": { "isValid": true, "errors": [] } } when the payload is good, or the list of problems to fix under data.errors — each with the offending field and a message. Same request body as checkout, but note the scopes differ: validation requires OrdersRead while the checkout itself requires OrdersCreate — provision both.
POST /v1/checkout
{
  "account": {
    "name": "Customer Name",
    "email": "customer@email.com",
    "phone": "555-123-4567",
    "shippingAddress": {
      "line1": "123 Main Street",
      "city": "Austin",
      "stateOrProvince": "TX",
      "postalCode": "78701",
      "country": "US"
    }
  },
  "order": {
    "externalOrderNumber": "YOUR-ORDER-ID",
    "orderType": "New",
    "lineItems": [
      {
        "sku": "1-P-SP-TX-XX-A-E-L-0325",
        "quantity": 1,
        "pricePerUnit": 0
      }
    ],
    "shippingAmount": 0,
    "taxAmount": 0,
    "payment": {
      "transactionId": "YOUR-ORDER-ID",
      "paymentDate": "2026-02-15T10:00:00Z",
      "amount": 0
    }
  }
}

Dropship: All pricing fields are $0 - PCC applies your contracted rates. Sending $0 requires a dropship-enabled token; without that flag, $0 prices are rejected with a 400.
Standard: Include actual prices. Total must match: lineItems + shipping + tax = payment.amount

{
  "order": {
    "externalOrderNumber": "YOUR-RENEWAL-ORDER-456",
    "orderType": "Renewal",
    "originalOrderNumber": "SO-2025-00001234",
    "lineItems": [
      {
        "sku": "1-P-SP-TX-XX-A-E-L-0326",
        "quantity": 1,
        "pricePerUnit": 0
      }
    ],
    "shippingAmount": 0,
    "taxAmount": 0,
    "payment": {
      "transactionId": "YOUR-RENEWAL-ORDER-456",
      "paymentDate": "2026-02-15T10:00:00Z",
      "amount": 0
    }
  }
}

Account is auto-resolved: For renewals, the shipping account is automatically looked up from the original order - you don't need to provide account details.
Update info on renewal: If you include account with new info (name, email, address), the account will be updated in our system and the new info used for this order.
Save the orderNumber from each order result to use as originalOrderNumber for future renewals.

{
  "account": {
    "name": "Test Customer",
    "email": "test@example.com",
    "shippingAddress": {
      "line1": "123 Test Street",
      "city": "Austin",
      "stateOrProvince": "TX",
      "postalCode": "78701",
      "country": "US"
    }
  },
  "order": {
    "externalOrderNumber": "TEST-ORDER-001",
    "orderType": "New",
    "testMode": true,
    "lineItems": [
      {
        "sku": "1-P-SP-TX-XX-A-E-L-0325",
        "quantity": 1,
        "pricePerUnit": 0
      }
    ],
    "shippingAmount": 0,
    "taxAmount": 0,
    "payment": {
      "transactionId": "TEST-ORDER-001",
      "paymentDate": "2026-02-15T10:00:00Z",
      "amount": 0
    }
  }
}

Test mode validates without creating records - request fields and product SKUs are verified, but no accounts, orders, or invoices are created in Dynamics. Note two things test mode does not exercise: tax is not calculated (the simulated total uses your submitted taxAmount or 0), and a renewal's originalOrderNumber is not looked up or ownership-checked - a bogus original order number will pass in test mode and fail in production.
Perfect for development and integration testing. Response includes "testMode": true and simulated IDs (e.g., TEST-...).
Important: use throwaway externalOrderNumber values for test submissions (e.g. a TEST- prefix) and never reuse a test run's number for the real order - test and live submissions are deduplicated separately, so each real order still needs an order number you have not used before.

Response (HTTP 202 Accepted): a successful checkout returns status 202, not 200 — the order is queued for asynchronous processing. Treat any 2xx as success.

{
  "success": true,
  "message": "Checkout queued successfully",
  "data": {
    "jobId": "job_69a1b2c3_a1b2c3d4",
    "status": "Pending",
    "externalOrderNumber": "YOUR-ORDER-ID",
    "testMode": false,
    "message": "Checkout order queued for processing",
    "statusUrl": "/v1/jobs/job_69a1b2c3_a1b2c3d4"
  }
}
What happens next: Orders are processed asynchronously. Save the jobId to check status. The jobId is an opaque string (e.g. job_69a1b2c3_a1b2c3d4) — store it as text, do not assume a UUID format.
Duplicate protection (idempotency): Each order.externalOrderNumber should be unique per order. If we receive another checkout with an externalOrderNumber we have already accepted for your account, we return the original job instead of creating a duplicate order — even if the duplicate requests arrive at the same time. This makes it safe to retry a request when a response is lost (timeout or network error): simply resend the same payload and you'll get the same jobId back. A job that ended in Failed, PermanentlyFailed, or Cancelled is not reused for dedup, so you can safely resubmit after a failure. We never create a duplicate order for the same externalOrderNumber. If a prior attempt already created the order but did not finish it, the resubmit does not create a second one — it returns an error identifying the existing order so it can be completed (don't keep resubmitting; contact support). If the prior attempt failed before creating an order, the resubmit creates one normally. For explicit control you may also send an Idempotency-Key request header. A given key maps to exactly one job: reusing the same key always returns that original job and its outcome — including if it failed. To retry after a failure, either resubmit with the same externalOrderNumber (which ignores failed jobs, as described above) or use a new Idempotency-Key.

Order Fields Reference

Field Required Description
accountNumber No PCC account number if known (skips account lookup). For renewals, auto-resolved from original order.
account.name New orders Customer or company name. For renewals the whole account object is optional - omit it entirely to keep the account unchanged, but if you include it, name, email, and a complete shippingAddress are all required together.
account.email New orders Customer email address. For renewals the whole account object is optional - omit it entirely to keep the account unchanged, but if you include it, name, email, and a complete shippingAddress are all required together.
account.shippingAddress New orders Where to ship. For renewals the whole account object is optional - omit it entirely to keep the account unchanged, but if you include it, name, email, and a complete shippingAddress are all required together.
account.partnerId Usually no Dynamics partner id for new orders. Your token is normally provisioned with a default partner id, in which case you omit this. If your token has no default, it is required on new orders and checkout returns a 400 asking for it — contact us for the value.
order.externalOrderNumber Yes Your unique order ID. Used for reference and duplicate protection — resending the same value returns the original order instead of creating a duplicate (see "Duplicate protection" above).
order.orderType No "New" (default) or "Renewal"
order.originalOrderNumber If renewal PCC order number from original order (e.g., "SO-2025-00001234")
order.testMode No Set to true for development testing (default: false)
order.lineItems Yes Products to order (sku, quantity, pricePerUnit)
order.payment Yes Payment reference (transactionId, paymentDate, amount)
5

Check Order Status

Poll the status endpoint to see when your order completes.

GET /v1/jobs/{jobId}
GET /v1/jobs/job_69a1b2c3_a1b2c3d4 HTTP/1.1
Host: api.postercompliance.com
Authorization: Bearer YOUR_API_TOKEN

Response (Completed):

{
  "success": true,
  "message": "Job retrieved successfully",
  "data": {
    "jobId": "job_69a1b2c3_a1b2c3d4",
    "jobType": "CheckoutOrder",
    "status": "Completed",
    "totalItems": 1,
    "processedItems": 1,
    "succeededItems": 1,
    "failedItems": 0,
    "progressPercent": 100,
    "createdAt": "2026-02-15T10:00:00Z",
    "startedAt": "2026-02-15T10:00:01Z",
    "completedAt": "2026-02-15T10:00:15Z",
    "errorMessage": null,
    "checkoutResults": {
      "success": true,
      "message": "Checkout completed successfully",
      "accountCreated": true,
      "account": {
        "accountId": "abc12345-1234-5678-9abc-def012345678",
        "accountNumber": "1234567",
        "name": "Customer Name",
        "isNew": true,
        "resolution": "Created new account"
      },
      "order": {
        "success": true,
        "message": "Order created successfully",
        "salesOrder": {
          "salesOrderId": "def45678-5678-9abc-def0-123456789abc",
          "orderNumber": "SO-2026-00001234",
          "subtotal": 164.98,
          "taxAmount": 13.61,
          "totalAmount": 178.59,
          "lineItemCount": 2,
          "lineItems": [
            {
              "lineItemId": "line-item-guid-1",
              "sku": "1-P-SP-TX-XX-A-E-L-0325",
              "productName": "Texas Labor Law Poster - 1 Year Plan",
              "quantity": 1,
              "pricePerUnit": 149.99,
              "extendedAmount": 149.99
            },
            {
              "lineItemId": "line-item-guid-2",
              "sku": "N-S-SH-SD-XX-X-X-X-0000",
              "productName": "Shipping Standard",
              "quantity": 1,
              "pricePerUnit": 14.99,
              "extendedAmount": 14.99
            }
          ]
        },
        "invoice": {
          "invoiceId": "inv-guid-12345",
          "invoiceNumber": "INV-2026-00001234",
          "totalAmount": 178.59,
          "taxAmount": 13.61,
          "status": "Posted"
        },
        "payment": {
          "paymentId": "pay-guid-12345",
          "amount": 178.59,
          "transactionId": "YOUR-ORDER-ID",
          "recorded": true
        },
        "processingSteps": [],
        "errors": []
      },
      "processingSteps": [],
      "errors": []
    }
  }
}
Key fields to save:
  • checkoutResults.account.accountNumber - Reuse for future orders from same customer
  • checkoutResults.order.salesOrder.orderNumber - Use to get tracking information

Status Values

Status Meaning Action
Pending Queued for processing Wait 5-10 seconds, check again
Processing Currently being processed Wait 5-10 seconds, check again
Completed Order created successfully Save the orderNumber
RetryPending A transient backend issue occurred; the order will be retried automatically Keep polling — no action needed
Failed Error occurred (validation, bad data, or a backend issue that was not safe to retry automatically). Resubmitting the same externalOrderNumber is always safe - duplicate protection ignores failed attempts. Check checkoutResults.message and checkoutResults.errors for the reason, fix and resubmit
PermanentlyFailed Failed after 5 retry attempts Contact support or resubmit manually
Handling failures: A transient backend issue (e.g. a temporary tax-service or Dynamics blip before the order is created) is retried for you automatically — the job shows RetryPending and is re-attempted up to 5 times with increasing delays (1, 5, 15, 30, 60 min), ending in PermanentlyFailed only if all retries are exhausted. Just keep polling; no resubmission needed. A Failed status means a non-retryable problem (validation, bad data) — check checkoutResults.errors, fix it, and resubmit. Resubmitting is safe: reusing the same order.externalOrderNumber returns the original order if one was created and never creates a duplicate (see "Duplicate protection" above).

Most orders complete within 30-60 seconds.

6

Get Tracking

Once an order ships, retrieve the tracking number to share with your customer.

GET /v1/orders/{orderNumber}/tracking
GET /v1/orders/SO-2026-00001234/tracking HTTP/1.1
Host: api.postercompliance.com
Authorization: Bearer YOUR_API_TOKEN

Response (Shipped):

{
  "success": true,
  "message": "Tracking information retrieved for order SO-2026-00001234",
  "data": {
    "orderNumber": "SO-2026-00001234",
    "trackingNumber": "1Z999AA10123456784",
    "found": true,
    "isRenewal": false,
    "errorMessage": null
  }
}

Response (Not Yet Shipped — HTTP 404):

{
  "error": "NotFound",
  "message": "Order 'SO-2026-00001234' not found or not in fulfilled status",
  "traceId": "0HN4ABC123"
}

Response (Renewal Order):

{
  "success": true,
  "message": "Renewal order SO-2026-00001234 - no shipping required",
  "data": {
    "orderNumber": "SO-2026-00001234",
    "trackingNumber": null,
    "found": true,
    "isRenewal": true,
    "errorMessage": "Renewal orders do not require shipping and will not have tracking numbers"
  }
}
When to check:
  • Orders typically ship within 1-3 business days
  • Start checking 24 hours after order submission
  • Check once every 4-6 hours until tracking is available
Renewal Orders:

Renewal orders do not require shipping and will not have tracking numbers. When you query tracking for a renewal order, it will return isRenewal: true with a message explaining that no shipping is required. You do not need to poll for tracking on renewal orders.

Bulk Tracking (Multiple Orders)

Check tracking for multiple orders at once:

POST /v1/orders/tracking

Request:

POST /v1/orders/tracking HTTP/1.1
Host: api.postercompliance.com
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

{
  "orderNumbers": [
    "SO-2026-00001234",
    "SO-2026-00001235",
    "SO-2026-00001236"
  ]
}

Response:

{
  "success": true,
  "message": "Tracking information retrieved",
  "data": {
    "totalRequested": 3,
    "totalFound": 2,
    "results": [
      {
        "orderNumber": "SO-2026-00001234",
        "trackingNumber": "1Z999AA10123456784",
        "found": true,
        "isRenewal": false,
        "errorMessage": null
      },
      {
        "orderNumber": "SO-2026-00001235",
        "trackingNumber": null,
        "found": true,
        "isRenewal": true,
        "errorMessage": "Renewal orders do not require shipping and will not have tracking numbers"
      },
      {
        "orderNumber": "SO-2026-00001236",
        "trackingNumber": null,
        "found": false,
        "isRenewal": false,
        "errorMessage": "Order not found or not fulfilled"
      }
    ]
  }
}
!

Error Handling

When something goes wrong, you'll get a clear error response:

{
  "error": "ValidationError",
  "message": "Shipping address is required",
  "details": null,
  "traceId": "0HN4ABC123",
  "timestamp": "2026-02-15T10:00:00Z",
  "validationErrors": {
    "account.shippingAddress": [
      "Shipping address is required"
    ]
  }
}
Reading errors: validationErrors is populated only for request-format (model-binding) failures. Business validation — missing account, unknown SKU, $0 price without the dropship flag — returns validationErrors: null with the specifics in details. Always read details as well, or you'll show a blank message for the most common checkout failures.

Rate & Request Limits

Limits apply per API token. When you exceed a rate limit you receive a 429 with a Retry-After header.

LimitValue
API requests (general)100 per minute
Job-status polling (/v1/jobs/...)300 per minute (separate bucket — polling never starves your submissions)
Line items per order500
Shipping destinations per estimate200 (500 items each)
Order numbers per bulk tracking request (POST /v1/orders/tracking)500
Polling guidance: after submitting an order you'll receive a job id. Poll its status every 5–10 seconds; most orders complete within 30–60 seconds. Faster polling gains nothing and consumes your polling budget.
Estimate tax honesty: if tax calculation fails for a destination, the estimate response sets allTaxCalculated: false with a taxCalculationWarning, and the affected destination carries taxCalculationSuccess: false with $0 tax. Do not treat those totals as final — retry the estimate.

HTTP Status Codes

Code Meaning What to Do
400 Bad request / validation error Check the error message and fix your request
401 Not authenticated Check your API token is correct
403 Not authorized Your token doesn't have access to this endpoint. A 403 with the message "The requested account or order is not available under this token" means the accountNumber or originalOrderNumber you sent doesn't belong to your account - double-check the value rather than your token.
404 Not found Check the order number or SKU exists
429 Too many requests (rate limit) Honor the Retry-After header (typically 60 seconds), then retry
500 Server error Retry later. Contact support if it persists.
502 / 503 / 504 A backend system we depend on is temporarily unavailable or slow Transient — retry with backoff (30–60s). For writes, see the retry note below: the request may already have applied, so retry only with the same Idempotency-Key.
Retrying writes safely: a 5xx or a timeout can happen after a write (checkout / order create) already applied, so a blind retry can create a duplicate order or invoice. Always send an Idempotency-Key on write requests and retry with the same key — a duplicate replays the original result instead of creating a second order. Reads (GET, estimate) are always safe to retry.

API Versioning & Changes

The API is versioned in the URL path (/v1/...). Within a version:

Need help? Include the traceId from the error response when contacting support.