Getting Started

The Sadler Beef Distribution API provides programmatic access to USDA beef inventory, pricing, and fulfillment. Build integrations to automate ordering, track shipments, and manage your supply chain.

Base URLs

Production
https://api.sadler.com/v1
Sandbox
https://sandbox.sadler.com/v1

Quick Example

curl https://api.sadler.com/v1/products \
 -H "Authorization: Bearer sk_live_..."

Authentication

Authenticate requests using your API key in the Authorization header. Each API key has a defined scope that determines which operations it can perform.

API Key Scopes

ScopeDescription
read_onlyRead products, prices, and orders
orders_writeCreate and modify orders
webhooks_manageConfigure webhook endpoints
curl https://api.sadler.com/v1/products \
 -H "Authorization: Bearer sk_live_4eC39HqLyjWDarjtT1zdp7dc"

Authentication Error

{
  "error": {
    "type": "authentication_error",
    "message": "API key invalid or expired"
  }
}

Note: API keys can be created, rotated, and revoked from the Sadler dashboard. Keep your API keys secure and never expose them in client-side code.

Versioning

The API uses header-based versioning. Specify the API version using the Sadler-Version header.

curl https://api.sadler.com/v1/products \
 -H "Authorization: Bearer sk_live_..." \
 -H "Sadler-Version: 2024-01-15"

Idempotency

Idempotency keys prevent duplicate requests. Include an Idempotency-Key header with a unique UUID to safely retry requests without creating duplicate resources.

Supported Endpoints

  • POST /orders
  • POST /payments
  • POST /shipments
curl -X POST https://api.sadler.com/v1/orders \
 -H "Authorization: Bearer sk_live_..." \
 -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
 -H "Content-Type: application/json" \
 -d '{"items": [{"product_id": "prod_123", "quantity": 10}]}'

Retention: Idempotency keys are retained for 24 hours. After this period, the same key can be reused.

Conflict Response

{
  "error": {
    "type": "conflict",
    "code": "idempotency_conflict",
    "message": "Request conflicts with existing idempotent request"
  }
}

Pagination

List endpoints return paginated results using cursor-based pagination. Use the cursor parameter to retrieve the next page of results.

Available Filters

GET /products

category, grade, origin, imps_code, updated_since, limit, cursor

GET /prices

sku, origin, updated_since

GET /orders

status, buyer_id, created_since, limit, cursor

curl "https://api.sadler.com/v1/products?cursor=eyJwYWdlIjoyfQ&limit=10" \
 -H "Authorization: Bearer sk_live_..."
{
  "data": [...],
  "has_more": true,
  "next_cursor": "eyJwYWdlIjozfQ"
}

Data Model

Product Object

FieldTypeDescription
idstringUnique product identifier
namestringProduct name
categorystringProduct category
pricenumberPrice in cents
inventorynumberAvailable quantity
lot_codestringProduction lot identifier
plant_est_numberstringUSDA plant establishment number
production_datestringProduction date (ISO 8601)
best_bystringBest by date (ISO 8601)

Order Object

FieldTypeDescription
idstringUnique order identifier
statusstringOrder status
itemsarrayOrder line items
totalnumberTotal in cents

Catch-weight Pricing

All beef items are sold per pound and finalized based on actual scale weight. Orders are created with estimated weights, and final pricing is adjusted when the product is weighed during fulfillment.

Order (Estimated)

{
  "items": [{
    "sku": "BF-RIBEYE-112A-CHOICE",
    "unit": "lb",
    "quantity": 150,
    "price_per_unit": 7.85,
    "price_basis": "per_lb",
    "weight_basis": "estimated"
  }]
}

Webhook (Finalized)

{
  "type": "order.finalized",
  "data": {
    "order_id": "ord_9f2a1c",
    "adjustments": {
      "actual_weight_lb": 158.6,
      "delta_amount": 61.21,
      "final_grand_total": 1257.21
    }
  }
}

Endpoints

List Products

GET/v1/products
curl https://api.sadler.com/v1/products \
 -H "Authorization: Bearer sk_live_..."

Create Order

POST/v1/orders
curl -X POST https://api.sadler.com/v1/orders \
 -H "Authorization: Bearer sk_live_..." \
 -H "Content-Type: application/json" \
 -d '{"items": [{"product_id": "prod_123", "quantity": 10}]}'

Create Quote

POST/v1/quotes

Create a temporary quote valid for 15 minutes. Use the quote_id when creating an order to lock pricing.

curl -X POST https://api.sadler.com/v1/quotes \
 -H "Authorization: Bearer sk_live_..." \
 -H "Content-Type: application/json" \
 -d '{"items": [{"sku":"BF-RIBEYE-112A-CHOICE","unit":"lb","quantity":150,"origin":"IA"}]}'
{
  "id": "qte_abc123",
  "expires_at": "2025-10-26T17:20:00Z",
  "totals": {
    "estimated_grand_total": 1196.00
  }
}

List Lots

GET/v1/lots

Retrieve lot and traceability information for products.

curl "https://api.sadler.com/v1/lots?sku=BF-RIBEYE-112A-CHOICE" \
 -H "Authorization: Bearer sk_live_..."

Webhooks

Webhooks allow you to receive real-time notifications about events in your account. Configure webhook endpoints in your dashboard.

Event Types

EventDescription
product.updatedProduct information changed
price.updatedPricing changed
order.createdNew order placed
order.finalizedOrder weight finalized
payment.capturedPayment processed
shipment.dispatchedOrder shipped
shipment.deliveredOrder delivered
order.expiredReservation expired

Signature Verification

Verify webhook signatures using HMAC SHA256. The signature is included in the Sadler-Signature header.

Header format: Sadler-Signature: t=timestamp, v1=signature

JavaScript

import crypto from "crypto";

function verifySig(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map(kv => kv.trim().split("="))
  );
  const payload = `${parts.t}.${rawBody}`;
  const digest = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  const safeEq = crypto.timingSafeEqual(
    Buffer.from(digest),
    Buffer.from(parts.v1)
  );
  const ageOk = Math.abs(Date.now()/1000 - Number(parts.t)) < 300;
  return safeEq && ageOk;
}

Python

import hmac, hashlib, time

def verify_sig(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(
        p.split("=",1) for p in [s.strip() for s in header.split(",")]
    )
    payload = f"{parts['t']}.{raw_body.decode()}".encode()
    digest = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return hmac.compare_digest(digest, parts["v1"]) and fresh

Retries: Failed webhooks are retried with exponential backoff for up to 24 hours. Return a 2xx status code to acknowledge receipt.

Errors

The API uses standard HTTP status codes and returns error details in JSON format with type, code, and message fields.

HTTPTypeCodeMessage
400validation_errorinvalid_quantityquantity must be greater than zero
401authentication_errorinvalid_api_keyAPI key is missing or invalid
403permission_errorscope_deniedkey lacks required scope
404not_foundsku_not_foundproduct not found
409conflictidempotency_conflictconflicting idempotent request
429rate_limitedtoo_many_requestsrate limit exceeded
500internal_errorinternalunexpected error

Rate Limits

The API enforces a default rate limit of 600 requests per minute per API key, with burst capacity up to 1200 requests per minute for 60 seconds.

Response Headers

X-RateLimit-Limit: 600
X-RateLimit-Remaining: 421
X-RateLimit-Reset: 1730001234

429 Response: When rate limited, respect the Retry-After header and use exponential backoff with jitter for retries.

Sandbox

Test your integration using the sandbox environment. No live funds move in sandbox mode, and test data is preloaded for development.

Sandbox URL: https://sandbox.api.sadler.com

Trigger Test Events

curl -X POST https://sandbox.api.sadler.com/sandbox/events \
 -H "Authorization: Bearer sk_test_..." \
 -H "Content-Type: application/json" \
 -d '{"type": "payment.captured", "order_id": "ord_test_1"}'

Changelog

2024-01-15

Latest
  • • Added webhook signature verification
  • • Improved error messages
  • • Added pagination support

2023-12-01

  • • Initial API release
  • • Products and Orders endpoints