Connect your platform to PCC for seamless labor law poster fulfillment
As a dropship partner, you send orders and we ship directly to your customers under your contracted rates. Here's your simplified flow:
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.
PCC will provide you with an API token. Include it in all requests:
Authorization: Bearer YOUR_API_TOKEN
Your token determines:
Retrieve our product catalog. Cache this data and refresh daily or weekly.
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
}
}
| 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) |
?productType=Plan for subscriptions only, or ?productType=Set for one-time purchases.
productNumber value as the sku field when placing checkout orders.
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.
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"
}
}
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.
For standard integrations where you collect payment, get the tax estimate first:
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
}
}
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.
Submit the order for fulfillment. Choose the tab that matches your scenario.
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.
/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.
{
"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"
}
}
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.
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.
| 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) |
Poll the status endpoint to see when your order completes.
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": []
}
}
}
checkoutResults.account.accountNumber - Reuse for future orders from same customercheckoutResults.order.salesOrder.orderNumber - Use to get tracking information| 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 |
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.
Once an order ships, retrieve the tracking number to share with your customer.
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"
}
}
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.
Check tracking for multiple orders at once:
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"
}
]
}
}
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"
]
}
}
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.
Limits apply per API token. When you exceed a rate limit you receive a 429 with a Retry-After header.
| Limit | Value |
|---|---|
| API requests (general) | 100 per minute |
Job-status polling (/v1/jobs/...) | 300 per minute (separate bucket — polling never starves your submissions) |
| Line items per order | 500 |
| Shipping destinations per estimate | 200 (500 items each) |
Order numbers per bulk tracking request (POST /v1/orders/tracking) | 500 |
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.
| 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. |
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.
The API is versioned in the URL path (/v1/...). Within a version:
traceId from the error response when contacting support.