ShedCloud Partner API
Reference

ShedCloud Partner API

A public, company-scoped HTTP API that lets your own developers and tools read this company’s data programmatically. Every credential is permanently bound to a single company — a credential issued by one company can never read another’s data.

Base URL

All requests go to your ShedCloud API host over HTTPS.

https://api.shedcloud.com

Environments

Two environments expose the identical /partner/v1 contract. Credentials are environment-specific — a production key never works against the sandbox and vice versa. Create sandbox credentials from the sandbox portal (Settings → Company → Developer API) and verify your integration there before pointing it at production.

Production
https://go.shedcloud.com
Sandbox
https://api.shedcloudtest.com

How it works

Authenticate with either an API key or an OAuth2 access token — both resolve to the same scoped identity, so every endpoint behaves identically no matter which you use. Requests are authorized by scopes granted to the credential.

example request
# List on-lot inventory
curl "__BASE__/partner/v1/lot-stock?limit=50" \
  -H "Authorization: Bearer sc_live_..."
// npm install @shedcloud/partner-api
import { ShedCloudPartnerClient } from "@shedcloud/partner-api";

const client = new ShedCloudPartnerClient({
  auth: { type: "apiKey", apiKey: "sc_live_..." },
});
const data = await client.lotStock.list({ limit: 50 });
// go get github.com/Corland-Partners-LLC/shedcloud-gomod/partnerapi
client, _ := partnerapi.New(partnerapi.Options{
    Auth: partnerapi.Auth{APIKey: "sc_live_..."},
})
data, err := client.LotStock.List(ctx, partnerapi.LotStockListParams{Limit: 50})
# pip install shedcloud-partner-api
from shedcloud_partner_api import ShedCloudPartnerClient

client = ShedCloudPartnerClient(
    auth={"type": "api_key", "api_key": "sc_live_..."},
)
data = client.lot_stock.list({"limit": 50})
// composer require shedcloud/partner-api
use ShedCloud\PartnerApi\{Auth, Client};

$client = new Client([
    'auth' => new Auth(apiKey: 'sc_live_...'),
]);
$data = $client->lotStock->list(['limit' => 50]);
# gem install shedcloud-partner_api
require 'shedcloud/partner_api'

client = ShedCloud::PartnerApi::Client.new(
  auth: ShedCloud::PartnerApi::Auth.new(api_key: 'sc_live_...'),
)
data = client.lot_stock.list(limit: 50)

Authentication

Two ways to authenticate, both resolving to the same { company, scopes } identity. Create and manage credentials in the ShedCloud portal under Settings → Company → Developer API (requires the partner-api.manage permission). Secrets are shown once at creation and only a hash is stored.

🔑

API key

One long-lived secret (sc_live_…). Best for a single server-side script you control. Send it directly as a bearer token.

OAuth2 app

A client_id + client_secret exchanged for short-lived access tokens. Best for integrations expecting the standard OAuth2 flow.

Sending the credential

Pass the API key or the OAuth access token in the Authorization header as a bearer token. That’s the only header the resource endpoints require.

Never embed secrets in client-side code or commit them to source control. Treat the create-time secret as the only copy — it cannot be retrieved again.
authorization header
# API key OR OAuth access token
curl "__BASE__/partner/v1/lot-stock" \
  -H "Authorization: Bearer <api_key_or_token>"
// The SDK adds the Authorization header on every request.
const client = new ShedCloudPartnerClient({
  auth: { type: "apiKey", apiKey: process.env.SHEDCLOUD_API_KEY },
});
// The SDK adds the Authorization header on every request.
client, err := partnerapi.New(partnerapi.Options{
  Auth: partnerapi.Auth{APIKey: os.Getenv("SHEDCLOUD_API_KEY")},
})

Rate limits & errors

Rate limiting is applied per credential (per API key / per token), not per IP. The defaults are 20 requests/second with a burst of 40. Exceeding the limit returns 429 with a Retry-After header (seconds) plus X-RateLimit-Limit and X-RateLimit-Remaining.

Every response also carries an X-Request-Id header (yours is echoed back when you send one) — include it when reporting issues.

OAuth access tokens live for 1 hour by default; expired tokens are rejected with 401. Revoking a credential (or rotating an app secret) takes effect on the very next request.

Error shape

All Partner API errors return a small JSON body: { "error": "<message>" }. The OAuth token endpoint uses the standard OAuth2 error fields instead.

401
Missing, invalid, revoked, or expired credential.
403
The credential lacks the scope the endpoint requires.
429
Per-credential rate limit exceeded — back off and retry.

Limits & timeouts

Request body
512 KB maximum on write endpoints.
Page size
100 rows maximum on lists (limit is clamped); 200 events maximum on the events feed.
Timeout
Requests are served within 30 seconds server-side — configure a 30–60 s client timeout.
Retention
Change events ~60 days · webhook delivery log ~30 days · idempotency-key replay window 24 hours.

Versioning & deprecation

Additive changes (new endpoints, optional parameters, response fields, event types) ship without notice — ignore unknown fields and unknown event types. Breaking changes only ship under a new version prefix (/partner/v2) with a minimum 6-month overlap window, announced to the technical contacts of companies with active credentials.

Support

Include the X-Request-Id response header when reporting issues — it links directly to the server-side request log. Reach us through your ShedCloud account contact or the portal’s Help → Support channel.

error responses
401 Unauthorized
{ "error": "invalid or expired credential" }
403 Forbidden
{ "error": "credential is not authorized for this scope" }
429 Too Many Requests
{ "error": "rate limit exceeded" }

Scopes

Scopes are granted per credential at creation. A request is allowed only if its credential holds the scope the endpoint requires. New scopes are added as more /partner/v1 resources ship.

partner-api.lot-stock.read
List on-lot inventory via GET /partner/v1/lot-stock.
partner-api.leads.read / .write
List and read leads; update lead fields and status.
partner-api.quotes.read / .write
List and read quotes; create in-stock quotes; update quote fields and status.
partner-api.orders.read / .write
List and read sales orders; update order fields and status.
partner-api.work-orders.read / .write
List and read work orders; update work order fields and status.
partner-api.locations.read / .write
List and read locations; create and update locations.
partner-api.customers.read / .write
List and read customers; create and update customers.
partner-api.products.read
List and read catalog products via GET /partner/v1/products.
partner-api.products.write
Create and update catalog products and their sizes via POST/PATCH /partner/v1/products.
partner-api.domains.read
List white-label storefront domains and their location/product mappings via GET /partner/v1/domains.
partner-api.users.read
List and read the company’s users/salespeople; list assignable roles.
partner-api.users.write
Create and update users (profile, role, locations, enable/disable) with the invite flow.
partner-api.contracts.read
Read the derived contract signature summary on orders.
partner-api.payments.read
List and read payments.
partner-api.payments.write
Record manual payments and create Stripe payment links on orders.
partner-api.documents.read
List documents and generate short-lived download links.
partner-api.events.read
Read the change-event feed and webhook delivery log; redeliver events.
partner-api.configurator-sessions.write
Create single-use 3D configurator launch sessions.
scope catalog
[
  "partner-api.lot-stock.read",
  "partner-api.leads.read",
  "partner-api.leads.write",
  "partner-api.quotes.read",
  "partner-api.quotes.write",
  "partner-api.orders.read",
  "partner-api.orders.write",
  "partner-api.work-orders.read",
  "partner-api.work-orders.write",
  "partner-api.locations.read",
  "partner-api.locations.write",
  "partner-api.customers.read",
  "partner-api.customers.write",
  "partner-api.products.read",
  "partner-api.products.write",
  "partner-api.users.read",
  "partner-api.users.write",
  "partner-api.contracts.read",
  "partner-api.payments.read",
  "partner-api.payments.write",
  "partner-api.documents.read",
  "partner-api.events.read",
  "partner-api.configurator-sessions.write",
  "partner-api.domains.read"
]
Getting started

Official libraries

ShedCloud maintains first-party SDKs for Node.js/TypeScript, Go, Python, PHP, and Ruby. All five cover the entire /partner/v1 surface — every resource on this page has a matching client method — and are released in lockstep with API additions (see the changelog for the SDK version that first supports each feature).

Node.js / TypeScript

@shedcloud/partner-api on npm. Fully typed, zero runtime dependencies, Node 18+.

Go

shedcloud-gomod/partnerapi. Context support and httptest-friendly design.

🐍

Python

shedcloud-partner-api on PyPI. Python 3.10+, snake_case params, httpx.

🐘

PHP

shedcloud/partner-api on Composer. PHP 8.2+, PSR-4 autoloading.

💎

Ruby

shedcloud-partner_api on RubyGems. Ruby 3.2+.

All SDKs handle authentication (API key or OAuth client credentials with automatic token refresh), environment selection (production / sandbox), typed errors, and idempotency keys on create calls. Webhook signature verification helpers are included in every package.

Prefer raw HTTP? Everything on this page works with plain curl — the SDKs are a convenience, not a requirement.

Machine & AI-readable docs

Building with an AI coding agent, or generating a client from the spec? The same documentation is published in machine-friendly formats: /partner/llms.txt (llms.txt index — point your agent here first), /partner/reference.md (the full reference as one Markdown file), /partner/openapi.json / .yaml (OpenAPI spec), and /partner/changelog.md.

install
npm install @shedcloud/partner-api
go get github.com/Corland-Partners-LLC/shedcloud-gomod/partnerapi
pip install shedcloud-partner-api
composer require shedcloud/partner-api
gem install shedcloud-partner_api
quickstart
import { ShedCloudPartnerClient } from "@shedcloud/partner-api";

const client = new ShedCloudPartnerClient({
  environment: "production", // or "sandbox"
  auth: { type: "apiKey", apiKey: process.env.SHEDCLOUD_API_KEY! },
});

const stock = await client.lotStock.list({ limit: 50 });
const quote = await client.quotes.create(
  { serialNumber: "SC-2024-00123", customer: { email: "[email protected]" } },
  { idempotencyKey: "order-42" }
);
import "github.com/Corland-Partners-LLC/shedcloud-gomod/partnerapi"

client, err := partnerapi.New(partnerapi.Options{
    Environment: partnerapi.EnvironmentProduction, // or EnvironmentSandbox
    Auth:        partnerapi.Auth{APIKey: os.Getenv("SHEDCLOUD_API_KEY")},
})

stock, err := client.LotStock.List(ctx, partnerapi.LotStockListParams{Limit: 50})
quote, err := client.Quotes.Create(ctx, partnerapi.QuoteCreateRequest{
    SerialNumber: "SC-2024-00123",
    Customer:     partnerapi.QuoteCreateCustomer{Email: "[email protected]"},
}, partnerapi.WithIdempotencyKey("order-42"))
from shedcloud_partner_api import ShedCloudPartnerClient

client = ShedCloudPartnerClient(
    environment="production",
    auth={"type": "api_key", "api_key": os.environ["SHEDCLOUD_API_KEY"]},
)
stock = client.lot_stock.list({"limit": 50})
quote = client.quotes.create(
    {"serial_number": "SC-2024-00123", "customer": {"email": "[email protected]"}},
    idempotency_key="order-42",
)
use ShedCloud\PartnerApi\{Auth, Client};

$client = new Client([
    'environment' => 'production',
    'auth' => new Auth(apiKey: getenv('SHEDCLOUD_API_KEY')),
]);
$stock = $client->lotStock->list(['limit' => 50]);
$quote = $client->quotes->create(
    ['serialNumber' => 'SC-2024-00123', 'customer' => ['email' => '[email protected]']],
    ['idempotencyKey' => 'order-42'],
);
client = ShedCloud::PartnerApi::Client.new(
  environment: 'production',
  auth: ShedCloud::PartnerApi::Auth.new(api_key: ENV.fetch('SHEDCLOUD_API_KEY')),
)
stock = client.lot_stock.list(limit: 50)
quote = client.quotes.create(
  { serialNumber: 'SC-2024-00123', customer: { email: '[email protected]' } },
  idempotency_key: 'order-42',
)
Endpoint

Issue an access token

POST /oauth/token

Exchanges OAuth2 client credentials for a short-lived bearer access token (the client_credentials grant, RFC 6749 §4.4). Send credentials via HTTP Basic auth or as form fields — both are supported.

Body parameters · application/x-www-form-urlencoded

grant_type
string
Required

Must be client_credentials.

client_id
string

OAuth client id. Omit if using HTTP Basic auth.

client_secret
string

OAuth client secret. Omit if using HTTP Basic auth.

There are no refresh tokens for this grant. When a token expires, simply request a new one.
request
curl -X POST "__BASE__/oauth/token" \
  -u "sc_client_xxx:sc_secret_xxx" \
  -d "grant_type=client_credentials"
// The SDK exchanges and refreshes tokens for you.
const client = new ShedCloudPartnerClient({
  auth: {
    type: "oauth",
    clientId: process.env.SHEDCLOUD_CLIENT_ID,
    clientSecret: process.env.SHEDCLOUD_CLIENT_SECRET,
  },
});
// The SDK exchanges and refreshes tokens for you.
client, err := partnerapi.New(partnerapi.Options{
  Auth: partnerapi.Auth{
    ClientID:     os.Getenv("SHEDCLOUD_CLIENT_ID"),
    ClientSecret: os.Getenv("SHEDCLOUD_CLIENT_SECRET"),
  },
})
response200 OK
{
  "access_token": "sc_at_xxxxxxxxxxxxxxxxxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "partner-api.lot-stock.read"
}
Endpoint

Credential introspection

GET /partner/v1/me

Returns who the calling credential belongs to and what it may do — company, credential type, granted scopes, and the per-credential rate-limit configuration. Requires any valid credential, no specific scope, making it the right first call to smoke-test a new integration or discover scopes without probing endpoints for 403s.

companyId / companyName
string

The company this credential is permanently bound to.

credentialType
string

api_key or oauth_client.

scopes
string[]

Scopes granted to this credential.

rateLimit
object

requestsPerSecond and burst — see Rate limits.

request
curl "__BASE__/partner/v1/me" \
  -H "Authorization: Bearer <token>"
const me = await client.me();
me, err := client.Me(ctx)
response200 OK
{
  "companyId": "6529b409e7d0f84e18e2a001",
  "companyName": "Leland's Sheds",
  "credentialType": "api_key",
  "scopes": ["partner-api.lot-stock.read", "partner-api.quotes.write"],
  "rateLimit": { "requestsPerSecond": 20, "burst": 40 }
}
Endpoint

List on-lot inventory

GET /partner/v1/lot-stock

Returns the authenticated company’s ready-to-sell physical inventory — on-lot units, rental returns, and immediate-sale units. Requires the partner-api.lot-stock.read scope.

Query parameters

page
integer · default 1

Page number (1-based).

limit
integer · default 50

Page size, maximum 100.

purchaseType
string · default ALL

Filter by ALL, Lot Stock, Rental Return, or Immediate Sale. Case-insensitive; hyphens/underscores accepted (lot-stock, immediate_sale). Unrecognized values fall back to ALL.

location
string

Filter by location id or slug.

region
string

Filter by the locations’ exact region label; combines with location (both must match).

search
string

Case-insensitive substring match on serial number or title.

sort
string · default createdAt

Sort by serialNumber, title, price, or createdAt.

order
string · default desc

Sort direction: asc or desc.

Only sellable units are returned — units with a processed linked sales order are excluded and results are collapsed to one row per work order.

Response fields

data[]
array

Inventory items (see example).

data[].attributes
object

Exterior configuration — siding, sidingColor, trimColor, roofMaterial, roofColor — the same values the work-order detail page shows. Custom colors surface as Custom Color - #HEX. Omitted when the unit has no configurator.

page / limit
integer

Echoed pagination values.

total
integer

Total matching records across all pages.

request
curl "__BASE__/partner/v1/lot-stock?limit=50&purchaseType=Lot%20Stock" \
  -H "Authorization: Bearer <token>"
const stock = await client.lotStock.list({
  limit: 50,
  purchaseType: "Lot Stock",
});
stock, err := client.LotStock.List(ctx, partnerapi.LotStockListParams{
  Limit:        50,
  PurchaseType: "Lot Stock",
})
response200 OK
{
  "data": [
    {
      "id": "6529b409e7d0f84e18e2a108",
      "workOrderId": "6529b409e7d0f84e18e2a222",
      "serialNumber": "SC-2024-00123",
      "title": "10x16 Lofted Barn",
      "purchaseType": "Lot Stock",
      "basePrice": 8200.00,
      "price": 8995.00,
      "locationId": "66c00443c2d8aa83c5757dcf",
      "locationName": "Dallas Lot",
      "locationSlug": "dallas-lot",
      "images": ["https://cdn.shedcloud.com/.../front.jpg"],
      "attributes": {
        "siding": "LP Smart Side Siding & Trim",
        "sidingColor": "Ivory",
        "trimColor": "Polar White",
        "roofMaterial": "Metal Roof",
        "roofColor": "Barn Red Metal"
      },
      "sold": false
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 137
}
Endpoint

Stock templates

GET /partner/v1/stock-templates
GET /partner/v1/stock-templates/{id}

The company’s buildable catalog designs (template work orders) — not physical inventory. Templates are deliberately excluded from lot-stock and rejected by quote-create; use them for a “design your own” catalog and deep-link the interactive 3D view via structuraUrl. Same scope as lot-stock: partner-api.lot-stock.read.

Query parameters

page / limit
integer

Pagination — limit max 60, default 50.

search
string

Case-insensitive match on work-order number, template name, or product name.

tags
string

Comma-separated public tags; templates must carry all of them.

tags in responses are the template’s public tags only. Prices mirror the public gallery: base price, upgrades total, and removed standard features. Non-template ids return 404.

request
curl "__BASE__/partner/v1/stock-templates?tags=barn,lofted&limit=20" \
  -H "Authorization: Bearer <token>"
const templates = await client.stockTemplates.list({
  tags: ["barn", "lofted"], limit: 20,
});
templates, err := client.StockTemplates.List(ctx, partnerapi.StockTemplateListParams{
  Tags:             []string{"barn", "lofted"},
  PaginationParams: partnerapi.PaginationParams{Limit: 20},
})
response200 OK
{
  "data": [
    {
      "id": "6529b409e7d0f84e18e2a555",
      "workOrderNumber": "T-1042",
      "templateName": "12x24 Side Lofted Barn",
      "productName": "Side Lofted Barn",
      "description": "Our most popular 12-wide layout…",
      "tags": ["barn", "lofted", "12-wide"],
      "images": ["https://cdn.shedcloud.com/.../hero.jpg"],
      "basePrice": 9200.00,
      "upgradesPrice": 850.00,
      "configuratorId": "6529b409e7d0f84e18e2a777",
      "structuraUrl": "https://3d.shedcloud.com/?...",
      "productId": "6529b409e7d0f84e18e2a888",
      "sizeId": "6529b409e7d0f84e18e2a999"
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 12
}
Endpoint

Leads

GET /partner/v1/leads
GET /partner/v1/leads/{id}

Lists and reads the company’s leads. Requires partner-api.leads.read.

Query parameters

page / limit
integer

Pagination — limit max 100, default 50.

search
string

Substring match on customer name, order number, email, or phone.

sort / order
string

orderNumber, customerName, status, total, createdAt (default), updatedAt · asc/desc.

status
string

e.g. Open, Lost, Retired (case-insensitive).

location
string

Location id or slug.

customerEmail / customerPhone / orderNumber / salesperson
string

Targeted field filters.

createdFrom/To · updatedFrom/To
date

RFC 3339 or YYYY-MM-DD date ranges.

request
curl "__BASE__/partner/v1/leads?status=Open&limit=25" \
  -H "Authorization: Bearer <token>"
const leads = await client.leads.list({ status: "Open", limit: 25 });

// One lead:
const lead = await client.leads.get("665f0a1b2c3d4e5f60718293");
leads, err := client.Leads.List(ctx, partnerapi.SalesListParams{
  Status: "Open",
  PaginationParams: partnerapi.PaginationParams{Limit: 25},
})

// One lead:
lead, err := client.Leads.Get(ctx, "665f0a1b2c3d4e5f60718293")
response200 OK
{
  "data": [
    {
      "id": "665f0a1b2c3d4e5f60718293",
      "orderNumber": 13901,
      "status": "Open",
      "customer": { "name": "Jane Doe", "email": "[email protected]", "phone": "555-0100" },
      "salesperson": { "name": "John Rep", "email": "[email protected]" },
      "location": { "id": "66c0...", "name": "Dallas Lot", "slug": "dallas-lot" },
      "createdAt": "2026-06-28T15:03:11Z",
      "updatedAt": "2026-07-01T18:22:04Z"
    }
  ],
  "page": 1, "limit": 25, "total": 8
}
Endpoint

Create a lead

POST /partner/v1/leads

Creates a lead at a sales location with the customer’s contact information. Requires partner-api.leads.write.

When salespersonName/salespersonEmail are omitted, the location’s lead routing strategy (round-robin, availability, skill-based) auto-assigns a salesperson — the same routing in-stock quote creation uses.

Body fields

locationId
string · required

Sales location id; must be one of your locations (404 otherwise).

customer
object · required

{ "name", "email", "phone" } — at least one of the three is required.

salespersonName
string

Explicit salesperson; skips lead routing.

salespersonEmail
string

Explicit salesperson email; skips lead routing.

Returns 201 with the created lead (same shape as GET /partner/v1/leads/{id}).

request
curl -X POST "__BASE__/partner/v1/leads" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "locationId": "66c00443c2d8aa83c5757dcf",
    "customer": { "name": "Jane Doe", "email": "[email protected]", "phone": "555-0100" }
  }'
const lead = await client.leads.create({
  locationId: "66c00443c2d8aa83c5757dcf",
  customer: { name: "Jane Doe", email: "[email protected]", phone: "555-0100" },
});
lead, err := client.Leads.Create(ctx, partnerapi.LeadCreateRequest{
  LocationID: "66c00443c2d8aa83c5757dcf",
  Customer: partnerapi.LeadCreateCustomer{
    Name:  "Jane Doe",
    Email: "[email protected]",
    Phone: "555-0100",
  },
})
created lead201 Created
{
  "id": "665f0a1b2c3d4e5f60718299",
  "orderNumber": 13902,
  "status": "Open",
  "customer": { "name": "Jane Doe", "email": "[email protected]", "phone": "555-0100" },
  "salesperson": { "name": "John Rep", "email": "[email protected]" },
  "location": { "id": "66c0...", "name": "Dallas Lot", "slug": "dallas-lot" }
}
Endpoint

Quotes

GET /partner/v1/quotes
GET /partner/v1/quotes/{id}
POST /partner/v1/quotes/{id}/line-items
DELETE /partner/v1/quotes/{id}/line-items/{lineId}

Lists and reads the company’s quotes. Requires partner-api.quotes.read. Accepts every lead-list parameter plus:

converted
boolean

true = only quotes converted to an order; false = only unconverted.

Quote items add pricing (subtotal, total, monthly payment, payment type), serialNumber, workOrderId, and conversion linkage (converted, convertedOrderId, convertedOrderNumber).

Quotes also carry an optional validUntil date (settable via PATCH or the portal). When it passes while the quote is still Open/Active a quote.expired event is emitted — the status is not changed automatically. A quote.sent event fires when the quote is shared/emailed to the customer; there is no quote viewed tracking. RTO quotes/orders include an rto block (term, monthly payment, rent, deposits, damage waiver, balance) — rto.providerName resolves on get-by-id only. There is no financing application lifecycle: the RTO provider handles approve/decline externally.

Line-item writes (scope partner-api.quotes.write): POST .../line-items adds one upgrade line (idempotent on lineKey), DELETE .../line-items/{lineId} removes one — the base product line is protected. Same semantics as on orders.

request
curl "__BASE__/partner/v1/quotes?converted=false&sort=total&order=desc" \
  -H "Authorization: Bearer <token>"
const quotes = await client.quotes.list({
  converted: false, sort: "total", order: "desc",
});

// Line items:
await client.quotes.addLineItem(quoteId, { productId, lineKey: "crm-42" });
await client.quotes.deleteLineItem(quoteId, lineId);
converted := false
quotes, err := client.Quotes.List(ctx, partnerapi.QuoteListParams{
  Converted:       &converted,
  SalesListParams: partnerapi.SalesListParams{Sort: "total", Order: partnerapi.SortDesc},
})

// Line items:
line, err := client.Quotes.AddLineItem(ctx, quoteID,
  partnerapi.LineItemCreateRequest{ProductID: productID, LineKey: "crm-42"})
err = client.Quotes.DeleteLineItem(ctx, quoteID, lineID)
quote item200 OK
{
  "id": "665f0a1b2c3d4e5f60718293",
  "orderNumber": 13847,
  "status": "Active",
  "customer": { "name": "Jane Doe", "email": "[email protected]" },
  "pricing": { "subtotal": 8200.00, "total": 8995.00, "monthlyPayment": 415.32, "paymentType": "rto" },
  "serialNumber": "SC-2024-00123",
  "converted": false
}
Endpoint

Create a quote from an in-stock unit

POST /partner/v1/quotes

Creates a quote for an on-lot building identified by serialNumber or workOrderId (both discoverable via GET /partner/v1/lot-stock). Requires partner-api.quotes.write.

The system runs the same pipeline ShedCloud’s own website embed uses: the customer is matched by email or created, the next quote number is issued, the unit’s work order is linked to the quote (serial number, product lines, and pricing are copied and the unit shows as taken), the sales location is assigned, and if the location has lead routing configured a salesperson is auto-assigned and notified by email. The customer also receives a confirmation email when an email address was provided.

Body fields

serialNumber
string

Serial number of the in-stock unit. Required unless workOrderId is sent.

workOrderId
string

Work order id of the unit; takes precedence when both are sent.

customer
object · required

{ "name", "email", "phone" } — an email, or a name + phone, is required. Matched by email when the customer already exists.

deliveryAddress
object

{ "address", "city", "state", "zipCode", "latitude", "longitude" } — stamped onto the quote and cascaded to the linked work order.

locationId
string

Sales location id. Defaults to the unit’s own location.

purchaseType
string

Lot Stock, Rental Return, or Immediate Sale; inferred from the unit when omitted.

price
number

Sell price; the unit’s own pricing is used when omitted.

note
string

Free-form note stored on the quote.

Returns 201 with the created quote (same shape as GET /partner/v1/quotes/{id}). Returns 400 when serialNumber and workOrderId point at different units, 404 when no work order matches the serial number or locationId isn’t one of your locations, and 409 when the unit already has an active quote or order, is a stock template, or is cancelled. Newly created customers are synced to any connected CRM.

request
curl -X POST "__BASE__/partner/v1/quotes" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "serialNumber": "SC-2024-00123",
    "customer": { "name": "Jane Doe", "email": "[email protected]", "phone": "555-0100" },
    "deliveryAddress": { "address": "42 Oak Ave", "city": "Dallas", "state": "TX", "zipCode": "75201" }
  }'
const quote = await client.quotes.create({
  serialNumber: "SC-2024-00123",
  customer: { name: "Jane Doe", email: "[email protected]", phone: "555-0100" },
  deliveryAddress: { address: "42 Oak Ave", city: "Dallas", state: "TX", zipCode: "75201" },
});
quote, err := client.Quotes.Create(ctx, partnerapi.QuoteCreateRequest{
  SerialNumber: "SC-2024-00123",
  Customer: partnerapi.QuoteCreateCustomer{
    Name: "Jane Doe", Email: "[email protected]", Phone: "555-0100",
  },
  DeliveryAddress: &partnerapi.QuoteCreateDeliveryAddress{
    Address: "42 Oak Ave", City: "Dallas", State: "TX", ZipCode: "75201",
  },
})
created quote201 Created
{
  "id": "665f0a1b2c3d4e5f60718293",
  "orderNumber": 13848,
  "status": "Open",
  "customer": { "name": "Jane Doe", "email": "[email protected]" },
  "location": { "id": "66c0...", "name": "Dallas Lot" },
  "serialNumber": "SC-2024-00123",
  "workOrderId": "665f0a1b2c3d4e5f60718294",
  "converted": false
}
Endpoint

Convert a quote to a sales order

POST /partner/v1/quotes/{id}/convert

Converts an open quote into a real sales order — the same conversion the portal’s “Place Order” button runs. Requires partner-api.orders.write (not the quotes scope: the endpoint creates a sales order).

The next order number is issued, the quote’s product lines and configurator are cloned onto the new order, the source quote is marked Sold with a back-reference (convertedOrderId / convertedOrderNumber), and a linked work order is re-linked to the order. The new order starts in Unsubmitted — submit it with POST /partner/v1/orders/{id}/statusUnprocessed when ready.

Body fields (optional)

salespersonName
string

Overrides the salesperson copied from the quote.

salespersonEmail
string

Overrides the salesperson email copied from the quote.

Returns 201 with the created order (same shape as GET /partner/v1/orders/{id}). Returns 409 when the quote is already Sold/Cancelled/Deleted or an active order already exists for it.

request
curl -X POST "__BASE__/partner/v1/quotes/665f0a1b2c3d4e5f60718293/convert" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
const order = await client.quotes.convert("665f0a1b2c3d4e5f60718293", {});
order, err := client.Quotes.Convert(ctx, "665f0a1b2c3d4e5f60718293",
  partnerapi.QuoteConvertRequest{})
created order201 Created
{
  "id": "665f0a1b2c3d4e5f60718296",
  "orderNumber": 13849,
  "status": "Unsubmitted",
  "customer": { "name": "Jane Doe", "email": "[email protected]" },
  "pricing": { "subtotal": 8200.00, "total": 8995.00, "paymentType": "rto" },
  "serialNumber": "SC-2024-00123",
  "workOrderId": "665f0a1b2c3d4e5f60718294",
  "sourceQuoteId": "665f0a1b2c3d4e5f60718293",
  "sourceQuoteNumber": 13848
}
Endpoint

Sales orders

GET /partner/v1/orders
GET /partner/v1/orders/{id}
POST /partner/v1/orders
POST /partner/v1/orders/{id}/line-items
DELETE /partner/v1/orders/{id}/line-items/{lineId}

Lists and reads the company’s sales orders. Requires partner-api.orders.read. Accepts every lead-list parameter plus:

paymentType
string

rto or cash.

serialNumber
string

Match on the unit serial number.

Order items add pricing (including changeOrderFee when a change-order fee was applied — change orders are a fee plus payment-ledger entries, not a separate order entity), deposits, serialNumber, workOrderId, source-quote linkage (sourceQuoteId, sourceQuoteNumber), and delivery dates (expectedDeliveryDate, deliveredAt).

Create

POST /partner/v1/orders (scope partner-api.orders.write) creates a full order directly, no quote needed: a customer (existing customerId or an inline customer block matched by email / created), locationId, the base productId (+ optional sizeId), upgrades lines, pricing header (basePrice / subtotal / total / paymentType), deliveryAddress, and an optional configuration payload — pass the portal-shaped selectedCategories array verbatim, or flat siding / sidingColor / trimColor / roofMaterial / roofColor attributes. Runs the portal’s own pipeline (order number, detail rows, configurator link, status log, ledger, audit, CRM). New orders start in Unsubmitted. Supports Idempotency-Key.

Line-item writes

POST .../line-items adds one upgrade line ({ productId, quantity, price, lineKey }) — idempotent on lineKey: re-sending the same key returns the existing line (200, "created": false). DELETE .../line-items/{lineId} removes one line by its key; the base product line is protected (409). The same pair exists on quotes. Header pricing is not recalculated — PATCH the entity’s pricing fields after changing lines.

request
curl "__BASE__/partner/v1/orders?status=Unprocessed&paymentType=rto" \
  -H "Authorization: Bearer <token>"
const orders = await client.orders.list({
  status: "Unprocessed", paymentType: "rto",
});
orders, err := client.Orders.List(ctx, partnerapi.OrderListParams{
  SalesListParams: partnerapi.SalesListParams{Status: "Unprocessed"},
  PaymentType:     "rto",
})
order item200 OK
{
  "id": "665f0a1b2c3d4e5f60718293",
  "orderNumber": 13847,
  "status": "Unprocessed",
  "pricing": { "subtotal": 8200.00, "total": 8995.00, "paymentType": "rto" },
  "serialNumber": "SC-2024-00123",
  "workOrderId": "665f0a1b2c3d4e5f60718294",
  "sourceQuoteId": "665f0a1b2c3d4e5f60718295"
}
create order
curl -X POST "__BASE__/partner/v1/orders" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4f0a1b2c-3d4e-5f60-7182-93a4b5c6d7e8" \
  -d '{
    "customer": { "name": "Jane Doe", "email": "[email protected]" },
    "locationId": "66c00443c2d8aa83c5757dcf",
    "productId": "6659f3ab8e5a2c001f9b1c11",
    "sizeId": "6659f3ab8e5a2c001f9b1c22",
    "basePrice": 8995,
    "paymentType": "cash",
    "upgrades": [{ "productId": "6659f3ab8e5a2c001f9b1c33", "quantity": 2 }],
    "configuration": { "sidingColor": "Barn Red", "roofMaterial": "Metal Roof", "roofColor": "Charcoal" }
  }'
const order = await client.orders.create(
  {
    customer: { name: "Jane Doe", email: "[email protected]" },
    locationId: "66c00443c2d8aa83c5757dcf",
    productId: "6659f3ab8e5a2c001f9b1c11",
    sizeId: "6659f3ab8e5a2c001f9b1c22",
    basePrice: 8995,
    paymentType: "cash",
    upgrades: [{ productId: "6659f3ab8e5a2c001f9b1c33", quantity: 2 }],
    configuration: { sidingColor: "Barn Red", roofMaterial: "Metal Roof", roofColor: "Charcoal" },
  },
  { idempotencyKey: "4f0a1b2c-3d4e-5f60-7182-93a4b5c6d7e8" },
);
basePrice := 8995.0
order, err := client.Orders.Create(ctx, partnerapi.OrderCreateRequest{
  Customer:    &partnerapi.QuoteCreateCustomer{Name: "Jane Doe", Email: "[email protected]"},
  LocationID:  "66c00443c2d8aa83c5757dcf",
  ProductID:   "6659f3ab8e5a2c001f9b1c11",
  SizeID:      "6659f3ab8e5a2c001f9b1c22",
  BasePrice:   &basePrice,
  PaymentType: "cash",
  Upgrades: []partnerapi.OrderCreateLineItem{
    {ProductID: "6659f3ab8e5a2c001f9b1c33", Quantity: 2},
  },
  Configuration: &partnerapi.OrderCreateConfiguration{
    SidingColor: "Barn Red", RoofMaterial: "Metal Roof", RoofColor: "Charcoal",
  },
}, partnerapi.WithIdempotencyKey("4f0a1b2c-3d4e-5f60-7182-93a4b5c6d7e8"))
add line item
201 Created
curl -X POST "__BASE__/partner/v1/orders/665f.../line-items" \
  -d '{ "productId": "6659f3ab8e5a2c001f9b1c44", "lineKey": "crm-line-42" }'

{ "lineId": "crm-line-42", "productId": "6659f3ab8e5a2c001f9b1c44", "quantity": 1, "created": true }
const line = await client.orders.addLineItem(orderId, {
  productId: "6659f3ab8e5a2c001f9b1c44",
  lineKey: "crm-line-42",
});

// Remove a line (the base product line is protected):
await client.orders.deleteLineItem(orderId, line.lineId);
line, err := client.Orders.AddLineItem(ctx, orderID, partnerapi.LineItemCreateRequest{
  ProductID: "6659f3ab8e5a2c001f9b1c44",
  LineKey:   "crm-line-42",
})

// Remove a line (the base product line is protected):
err = client.Orders.DeleteLineItem(ctx, orderID, line.LineID)
Endpoint

Work orders

GET /partner/v1/work-orders
GET /partner/v1/work-orders/{id}
POST /partner/v1/work-orders

Lists and reads the company’s manufacturing work orders. Reads require partner-api.work-orders.read; create requires partner-api.work-orders.write.

Query parameters

page / limit / order

Same as the sales lists.

search
string

Substring match on serial number, title, or work order number.

sort
string · default createdAt

workOrderNumber, serialNumber, status, createdAt, updatedAt.

status
string

e.g. Open build, Build in process, Finished good, Delivered.

serialNumber / orderNumber / linkedOrderId / location
string

Targeted filters; linkedOrderId is the exact linked sales order id.

createdFrom/To · updatedFrom/To
date

RFC 3339 or YYYY-MM-DD date ranges.

Delivery block

GET /partner/v1/work-orders/{id} adds a delivery object when the unit has been placed on a transportation run: scheduledDate, runNumber, runStatus (Scheduled · Dispatched · Delivered · Cancelled), driverName, and deliveredAt. Omitted on list responses and for units never scheduled.

Create

POST /partner/v1/work-orders creates a work order the way the portal’s building wizard does: status starts in Customer Care and the number is allocated automatically. locationId (id or slug) is required; friendly type enums map to the internal build-type flags:

purchaseType
string

new-build · existing-physical-inventory · general · stock.

workOrderType
string

made-to-order · lot-stock · rental-return · immediate-sale · template.

deliveryType
string

delivery-from-factory · built-on-site · delivered-from-dealer · delivered-from-lot-stock.

sizeId
string

Optional product size to attach — also creates the manufacturing BOM unless purchaseType is existing-physical-inventory.

serialNumber
string

Optional; unique per company — duplicates return 409. Plus optional title, promisedDate, externalReferences.

workOrderType: template creates a stock template (internal-only Template status, appears under stock templates once published). Supports Idempotency-Key.

request
curl "__BASE__/partner/v1/work-orders?status=Build%20in%20process" \
  -H "Authorization: Bearer <token>"
const workOrders = await client.workOrders.list({
  status: "Build in process",
});
workOrders, err := client.WorkOrders.List(ctx, partnerapi.WorkOrderListParams{
  Status: "Build in process",
})
work order item200 OK
{
  "id": "665f0a1b2c3d4e5f60718294",
  "workOrderNumber": 5012,
  "serialNumber": "SC-2024-00123",
  "title": "10x16 Lofted Barn",
  "status": "Build in process",
  "orderId": "665f0a1b2c3d4e5f60718293",
  "orderNumber": 13847,
  "location": { "id": "66c0...", "name": "Dallas Lot" },
  "basePrice": 8200.00,
  "promisedDate": "2026-08-15T00:00:00Z"
}
create work order
curl -X POST "__BASE__/partner/v1/work-orders" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -d '{
    "locationId": "dallas-lot",
    "purchaseType": "new-build",
    "workOrderType": "made-to-order",
    "deliveryType": "delivery-from-factory",
    "serialNumber": "SC-2026-00999",
    "sizeId": "6659f3ab8e5a2c001f9b1c22",
    "promisedDate": "2026-09-01"
  }'
const workOrder = await client.workOrders.create(
  {
    locationId: "dallas-lot",
    purchaseType: "new-build",
    workOrderType: "made-to-order",
    deliveryType: "delivery-from-factory",
    serialNumber: "SC-2026-00999",
    sizeId: "6659f3ab8e5a2c001f9b1c22",
    promisedDate: "2026-09-01",
  },
  { idempotencyKey: "7c9e6679-7425-40de-944b-e07fc1f90ae7" },
);
workOrder, err := client.WorkOrders.Create(ctx, partnerapi.WorkOrderCreateRequest{
  LocationID:    "dallas-lot",
  PurchaseType:  "new-build",
  WorkOrderType: "made-to-order",
  DeliveryType:  "delivery-from-factory",
  SerialNumber:  "SC-2026-00999",
  SizeID:        "6659f3ab8e5a2c001f9b1c22",
  PromisedDate:  "2026-09-01",
}, partnerapi.WithIdempotencyKey("7c9e6679-7425-40de-944b-e07fc1f90ae7"))
Endpoint

Locations

GET /partner/v1/locations
GET /partner/v1/locations/{id}
POST /partner/v1/locations
PATCH /partner/v1/locations/{id}

Lists, reads, creates, and updates the company’s locations (sales lots, plants, warehouses). Reads require partner-api.locations.read; writes require partner-api.locations.write.

List query parameters

page / limit / order

Same as the sales lists.

search
string

Substring match on name, code, or city.

sort
string · default createdAt

name, code, city, createdAt, updatedAt.

active / salesLot / plant
boolean

Flag filters.

region
string

Exact match on the location’s dealer-defined region label. The same label filters lot-stock.

Location items include region (dealer-defined grouping label, edited in the portal’s location edit view) and timezone (IANA name, currently the company-level timezone — interpret storeHours in this zone). Locations flagged “Hide from Partner API” in the portal never appear in any partner response.

Create

name is required, plus either an address or a latitude/longitude pair (provided together). code must be unique in the company (409 on duplicates). slug is derived from the name when omitted. Returns 201 with the created location.

Update

PATCH accepts the same field set as create (all optional): name, slug, code, address, city, state, zipCode, phone, contactPerson, contactEmail, latitude, longitude, active, salesLot, plant. Unknown fields are rejected with 400.

Store hours

storeHours is the weekly store schedule keyed by day (mon..sun) with 24-hour "HH:MM" times in the location’s local time. It’s omitted when the location has no schedule configured; disabled days may still carry their last from/to values.

create request
curl -X POST "__BASE__/partner/v1/locations" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Fort Worth Lot", "code": "FTW01", "address": "500 Elm St", "city": "Fort Worth", "state": "TX", "zipCode": "76102", "salesLot": true }'
const location = await client.locations.create({
  name: "Fort Worth Lot",
  code: "FTW01",
  address: "500 Elm St",
  city: "Fort Worth", state: "TX", zipCode: "76102",
  salesLot: true,
});

// Update:
await client.locations.update(location.id, { phone: "555-0199" });
salesLot := true
location, err := client.Locations.Create(ctx, partnerapi.LocationCreateRequest{
  Name:     "Fort Worth Lot",
  Code:     "FTW01",
  Address:  "500 Elm St",
  City:     "Fort Worth", State: "TX", ZipCode: "76102",
  SalesLot: &salesLot,
})

// Update:
location, err = client.Locations.Update(ctx, location.ID,
  partnerapi.LocationPatchRequest{Phone: "555-0199"})
location item200 OK
{
  "id": "66c00443c2d8aa83c5757dcf",
  "name": "Dallas Lot",
  "slug": "dallas-lot",
  "code": "DAL01",
  "address": "123 Main St",
  "city": "Dallas",
  "state": "TX",
  "zipCode": "75201",
  "phone": "555-0100",
  "latitude": 32.7767,
  "longitude": -96.7970,
  "active": true,
  "salesLot": true,
  "plant": false,
  "storeHours": {
    "mon": { "enabled": true, "from": "08:00", "to": "17:00" },
    "sat": { "enabled": true, "from": "09:00", "to": "13:00" },
    "sun": { "enabled": false }
  }
}
Endpoint

Customers

GET /partner/v1/customers
GET /partner/v1/customers/{id}
POST /partner/v1/customers
PATCH /partner/v1/customers/{id}
POST /partner/v1/customers/{id}/merge

Lists, reads, creates, updates, and merges the company’s customers. Reads require partner-api.customers.read; writes require partner-api.customers.write.

List query parameters

page / limit / order

Same as the sales lists.

search
string

Substring match on name, email, or phone.

email / phone
string

Targeted substring filters.

sort
string · default createdAt

name, email, createdAt, updatedAt.

createdFrom/To · updatedFrom/To
date

RFC 3339 or YYYY-MM-DD date ranges.

includeMerged
boolean · default false

Merged-away customers are hidden from lists by default; pass true to include them.

Create

email is required and must be unique in the company (409 on duplicates). Optional: name, contactName, contactPerson, phone, address, city, state, zipCode, code (random 5-char code when omitted). New customers sync to any connected CRM, same as portal-created customers. Returns 201.

Update

PATCH accepts the create field set plus active (boolean). Unknown fields are rejected with 400. Patching a customer that has already been merged away returns 409.

Merge duplicates

POST /partner/v1/customers/{id}/merge with body { "into": "<survivor id>" } folds the customer {id} (the “loser”) into the survivor: all leads, quotes, and orders are relinked to the survivor, the loser is deactivated and stamped with lineage (merged: true, mergedInto, mergedAt — visible on get-by-id), and a customer.merged event is emitted. Merging is not reversible via the API. Returns the survivor; 409 when either side is already merged or the ids are equal. Supports idempotency keys.

create request
curl -X POST "__BASE__/partner/v1/customers" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]", "name": "Jane Doe", "phone": "555-0100" }'
const customer = await client.customers.create({
  email: "[email protected]",
  name: "Jane Doe",
  phone: "555-0100",
});

// Update / merge duplicates:
await client.customers.update(customer.id, { city: "Dallas" });
await client.customers.merge(duplicateId, { survivorId: customer.id });
customer, err := client.Customers.Create(ctx, partnerapi.CustomerCreateRequest{
  Email: "[email protected]",
  Name:  "Jane Doe",
  Phone: "555-0100",
})

// Update / merge duplicates:
customer, err = client.Customers.Update(ctx, customer.ID,
  partnerapi.CustomerPatchRequest{City: "Dallas"})
survivor, err := client.Customers.Merge(ctx, duplicateID,
  partnerapi.CustomerMergeRequest{SurvivorID: customer.ID})
customer item200 OK
{
  "id": "665f0a1b2c3d4e5f60718293",
  "name": "Jane Doe",
  "email": "[email protected]",
  "phone": "555-0100",
  "address": "42 Oak Ave",
  "city": "Dallas",
  "state": "TX",
  "zipCode": "75201",
  "code": "A1B2C",
  "active": true,
  "createdAt": "2026-06-28T15:03:11Z"
}
Endpoint

Products

GET /partner/v1/products
GET /partner/v1/products/{id}
POST /partner/v1/products
PATCH /partner/v1/products/{id}
POST /partner/v1/products/{id}/sizes

Lists and reads the company’s finished catalog products — the same list the portal shows under Inventory → Products → Products. Raw materials and kits are never returned (a raw/kit id on the get-one endpoint returns 404). Reads require partner-api.products.read. For serialized on-lot units, use lot stock instead.

Each product carries images: ready-to-use public URLs — the uploaded gallery (in upload order) first, then any legacy image slots on the product record, deduplicated.

Writes (scope partner-api.products.write)

Create (name required) makes a finished catalog product; the optional lineId links it under an existing product line. PATCH updates allowlisted fields: name, description, sku, price, width, length, active — changes propagate to every localized copy and the denormalized relationship rows the portal reads.

Sizes: for configurable models, real pricing and dimensions live on size children, not the parent. POST .../{id}/sizes (width, length, price required; name defaults to <width>x<length>) creates a size and links it with the portal’s finished-good relationship. Sizes never appear in the products list. Both creates accept Idempotency-Key.

Query parameters

page / limit / order

Same as the sales lists.

search
string

Substring match on name, SKU, or description.

sku
string

Filter by SKU (substring).

active
boolean

Filter by active flag.

sort
string · default createdAt

name, sku, price, createdAt, updatedAt.

createdFrom/To · updatedFrom/To
date

RFC 3339 or YYYY-MM-DD date ranges.

request
curl "__BASE__/partner/v1/products?search=barn&sort=price&order=asc" \
  -H "Authorization: Bearer <token>"
const products = await client.products.list({
  search: "barn", sort: "price", order: "asc",
});

// Writes:
const product = await client.products.create({ name: "12x20 Garage", price: 11500 });
await client.products.update(product.id, { sku: "GR-1220" });
await client.products.createSize(product.id, { width: 12, length: 24, price: 13200 });
products, err := client.Products.List(ctx, partnerapi.ProductListParams{
  Search: "barn", Sort: "price", Order: partnerapi.SortAsc,
})

// Writes:
price := 11500.0
product, err := client.Products.Create(ctx, partnerapi.ProductCreateRequest{
  Name: "12x20 Garage", Price: &price,
})
product, err = client.Products.Update(ctx, product.ID,
  partnerapi.ProductPatchRequest{SKU: "GR-1220"})
product item200 OK
{
  "id": "6529b409e7d0f84e18e2a108",
  "name": "10x16 Lofted Barn",
  "description": "Lofted barn with double doors",
  "sku": "LB-1016",
  "price": 8200.00,
  "width": 10,
  "length": 16,
  "active": true,
  "images": [
    "https://cdn.shedcloud.com/public/products/front.jpg",
    "https://cdn.shedcloud.com/public/products/interior.jpg"
  ]
}
Endpoint

Domains

GET /partner/v1/domains
GET /partner/v1/locations/{id}/domains

Read-only view of the company’s white-label storefront subdomains (configured under Settings → Integrations → DNS) with their per-location assignments: hide-prices, product/size mappings, and the defaultForStore flag — a location’s primary storefront domain (at most one per location, managed from the location’s Domains tab). Requires partner-api.domains.read.

One item per subdomain; a DNS integration with several subdomains produces several items sharing the same integrationId. Empty sizes on a product mapping means all sizes are offered. The per-location endpoint (and the locationId / defaultForStore filters) narrow locations to the matching entries.

Query parameters

page / limit
integer

Pagination — domains are a small set, paged in memory.

locationId
string

(list only) Narrow to domains assigned to this location.

defaultForStore
boolean

true → only domains flagged Default For Store for their location(s).

request
curl "__BASE__/partner/v1/domains?defaultForStore=true" \
  -H "Authorization: Bearer <token>"
const domains = await client.domains.list({ defaultForStore: true });

// Domains assigned to one location:
const forLocation = await client.domains.forLocation(locationId, {});
defaultForStore := true
domains, err := client.Domains.List(ctx, partnerapi.DomainListParams{
  DefaultForStore: &defaultForStore,
})

// Domains assigned to one location:
forLocation, err := client.Domains.ForLocation(ctx, locationID,
  partnerapi.LocationDomainListParams{})
domain item200 OK
{
  "integrationId": "6613f2ab9cd0e21a34b7c001",
  "subdomain": "shop.lelandsheds.com",
  "apexDomain": "lelandsheds.com",
  "verified": true,
  "locations": [
    {
      "locationId": "66194eb4757eda220e773709",
      "name": "Leland Main Lot",
      "code": "LML",
      "hidePrices": false,
      "defaultForStore": true,
      "products": [
        {
          "productId": "6529b409e7d0f84e18e2a108",
          "sizes": ["6529b409e7d0f84e18e2a201"],
          "excludedUpgrades": ["6529b409e7d0f84e18e2a305"]
        }
      ]
    }
  ]
}
Endpoint

B2B agreements

GET /partner/v1/agreements
GET /partner/v1/agreements/{id}
GET /partner/v1/agreements/active
GET /partner/v1/agreements/{id}/state-legal
GET /partner/v1/agreements/{id}/state-legal/{state}

Read-only view of the company’s B2B partnership agreements (dealer ↔ RTO / ERP / DMAN). Invitations, acceptance, and rate proposals stay on the portal. Requires partner-api.agreements.read. Acceptance evidence and internal actor ids are never included. Cross-tenant ids return 404.

RTO quotes/orders now expose rto.agreementId (the linked agreement on the sales order). rto.providerName still resolves on get-by-id only.

Query parameters (list)

page / limit
integer

Pagination — limit max 100.

status
string

Filter by status (active, pending, revoked, …).

search
string

Substring match on counterparty name.

request
curl "__BASE__/partner/v1/agreements?status=active" \
  -H "Authorization: Bearer <token>"

curl "__BASE__/partner/v1/agreements/active" \
  -H "Authorization: Bearer <token>"

curl "__BASE__/partner/v1/agreements/{id}/state-legal/TX" \
  -H "Authorization: Bearer <token>"
const agreements = await client.agreements.list({ status: 'active' });
const active = await client.agreements.active();
const txLegal = await client.agreements.getStateLegal(agreementId, 'TX');
agreements, err := client.Agreements.List(ctx, partnerapi.AgreementListParams{
  Status: "active",
})
active, err := client.Agreements.Active(ctx)
txLegal, err := client.Agreements.GetStateLegal(ctx, agreementID, "TX")
agreement item200 OK
{
  "id": "665f0a1b2c3d4e5f60718293",
  "direction": "DMAN_RTO",
  "status": "active",
  "from": { "companyId": "66194eb4757eda220e773709", "name": "Leland Sheds" },
  "to": { "companyId": "6613f2ab9cd0e21a34b7c001", "name": "AFG Rentals, LLC" },
  "rateConfig": {
    "states": [{ "stateCode": "TX", "termRates": [{ "months": 36, "rate": 0.0899 }] }]
  },
  "orderCount": 42,
  "hasOrders": true
}
Endpoint

Users / salespeople

GET /partner/v1/users
GET /partner/v1/users/{id}
POST /partner/v1/users
PATCH /partner/v1/users/{id}
GET /partner/v1/roles

The company’s users — resolve salesperson.id on sales entities, see lead-routing membership, and manage users. Reads require partner-api.users.read, writes partner-api.users.write.

page / limit
integer

Pagination — limit max 100.

search
string

Substring match on name or email.

active
boolean

Filter by active flag.

allLocations: true means the user isn’t restricted to specific locations. inLeadRoutingPool reflects automatic lead-assignment membership.

Create a user

POST /partner/v1/users runs the portal’s own pipeline: profile, RBAC role, location assignments, and the invitation email (default on — pass "invite": false to skip). Two behaviors to know: an email belonging to an existing ShedCloud account is linked to your company instead of duplicated (409 when already a member), and no login is created here — the user’s Cognito account is created at their first sign-in through the invite. Accepts Idempotency-Key.

firstName / lastName / email
string · required

Profile. The email doubles as the username.

roleId
string · required

Company RBAC role id — discover via GET /partner/v1/roles.

locationIds / allLocations
string[] / boolean

Restrict to specific locations, or grant unrestricted access. Mutually exclusive.

Update a user

PATCH /partner/v1/users/{id} accepts the same profile fields plus roleId (replaces the role), locationIds (replaces assignments), allLocations, and active (per-company enable/disable — the company owner is protected).

user item200 OK
{
  "id": "66a0aa0bb1cc2dd3ee4ff556",
  "name": "John Rep",
  "email": "[email protected]",
  "phone": "555-0100",
  "active": true,
  "locationIds": ["66c00443c2d8aa83c5757dcf"],
  "allLocations": false,
  "inLeadRoutingPool": true
}
create a user
curl -X POST "https://go.shedcloud.com/partner/v1/users" \
  -H "Authorization: Bearer sc_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "firstName": "Alex", "lastName": "Rep",
    "email": "[email protected]",
    "roleId": "665f0a1b2c3d4e5f60718293",
    "locationIds": ["66c00443c2d8aa83c5757dcf"]
  }'
const roles = await client.users.roles();
const user = await client.users.create(
  {
    firstName: "Alex", lastName: "Rep",
    email: "[email protected]",
    roleId: roles.data[0].id,
    locationIds: ["66c00443c2d8aa83c5757dcf"],
  },
  { idempotencyKey: crypto.randomUUID() },
);

// Update:
await client.users.update(user.id, { active: false });
roles, err := client.Users.Roles(ctx)
user, err := client.Users.Create(ctx, partnerapi.UserCreateRequest{
  FirstName:   "Alex",
  LastName:    "Rep",
  Email:       "[email protected]",
  RoleID:      roles.Data[0].ID,
  LocationIDs: []string{"66c00443c2d8aa83c5757dcf"},
}, partnerapi.WithIdempotencyKey(uuid.NewString()))

// Update:
inactive := false
user, err = client.Users.Update(ctx, user.ID,
  partnerapi.UserPatchRequest{Active: &inactive})
role item200 OK
// GET /partner/v1/roles
{
  "id": "665f0a1b2c3d4e5f60718293",
  "name": "Salesperson",
  "key": "salesperson",
  "isSystem": true
}
Endpoint

Status history, line items & contract

GET /partner/v1/{leads|quotes|orders|work-orders}/{id}/status-history
GET /partner/v1/{quotes|orders}/{id}/line-items
GET /partner/v1/orders/{id}/contract

Status history

The full status timeline of a record — every transition from every source (portal, automation, Partner API), with the actor and an optional note. Uses the parent resource’s read scope.

Line items & configuration

The curated product lines on a quote/order (status: included standard feature, added upgrade, or removed option) plus a configuration block summarizing the configured building (model, siding, colors, roof). configuration is omitted when there’s no linked configurator.

Contract summary

Derived, read-only signature status: draft, out_for_signature, partially_signed, or completed, with per-party signed flags/dates. Requires partner-api.contracts.read. No signature images and no customer-data-sheet PII are ever exposed. When present, signedPdfDocumentId works with the documents download endpoint.

status history200 OK
{
  "data": [
    {
      "status": "Unprocessed",
      "previousStatus": "Unsubmitted",
      "description": "Submitted by partner integration",
      "changedAt": "2026-07-01T18:22:04Z",
      "actor": { "name": "John Rep", "email": "[email protected]" }
    }
  ],
  "page": 1, "limit": 50, "total": 3
}
contract summary200 OK
{
  "orderId": "665f0a1b2c3d4e5f60718293",
  "status": "completed",
  "customerSigned": true,
  "customerSignedAt": "2026-07-02T16:40:00Z",
  "salespersonSigned": true,
  "signedPdfDocumentId": "66aa11bb22cc33dd44ee55ff"
}
Endpoint

Payments

GET /partner/v1/payments
GET /partner/v1/payments/{id}
GET /partner/v1/orders/{id}/payments
POST /partner/v1/orders/{id}/payments
POST /partner/v1/orders/{id}/payment-links

Payment records — card/ACH via Stripe checkout plus manually recorded payments. Reads require partner-api.payments.read, writes partner-api.payments.write. Stripe payloads are redacted; only the opaque providerReference is exposed.

orderId
string

Filter by sales order id.

status
string

Case-insensitive exact match, e.g. paid, Refunded.

createdFrom / createdTo
date

RFC 3339 or YYYY-MM-DD.

method is card, ach, cash, check, financed, or manual. Orders also carry a deposits block (and an rto block on RTO sales) directly on the order DTO.

Record a manual payment

POST /partner/v1/orders/{id}/payments records a payment through the portal’s own pipeline — payment record, order-balance ledger recalc, audit — and fires payment.created automatically. Status is always paid. Accepts Idempotency-Key.

method
string · required

cash, check, financed, or manual. card/ach are rejected — those records are created by the Stripe pipeline (use payment links).

amount
number · required

Payment amount (> 0).

description
string

Defaults to "<Method> payment".

checkNumber / bankName
string

For methods check and financed respectively.

Stripe payment links

POST /partner/v1/orders/{id}/payment-links creates a Stripe Checkout session (requires the company’s Stripe integration with payments enabled — 503 otherwise) and returns the hosted payment URL. The webhook pipeline records the payment when the customer pays. The customer payment-link email sends by default; pass "sendEmail": false to skip it.

amount
number · required

Amount to collect (minimum 1).

name
string

Checkout line-item label (defaults to "Order #<n>").

email
string

Recipient override; defaults to the order’s customer email.

sendEmail
boolean · default true

Email the link to the customer.

currency
string · default usd

ISO currency code.

payment item200 OK
{
  "id": "66bb22cc33dd44ee55ff6677",
  "orderId": "665f0a1b2c3d4e5f60718293",
  "amount": 1314.82,
  "method": "card",
  "status": "paid",
  "providerReference": "cs_live_a1B2c3...",
  "createdAt": "2026-07-01T18:25:11Z"
}
record a manual payment
# check payment; retried requests with the same key replay
curl -X POST "https://go.shedcloud.com/partner/v1/orders/665f.../payments" \
  -H "Authorization: Bearer sc_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"method": "check", "amount": 500, "checkNumber": "1042"}'
const payment = await client.orders.createPayment(
  orderId,
  { method: "check", amount: 500, checkNumber: "1042" },
  { idempotencyKey: crypto.randomUUID() },
);

// Stripe Checkout link:
const link = await client.orders.createPaymentLink(orderId, { amount: 250 });
payment, err := client.Orders.CreatePayment(ctx, orderID,
  partnerapi.PaymentCreateRequest{
    Method: "check", Amount: 500, CheckNumber: "1042",
  }, partnerapi.WithIdempotencyKey(uuid.NewString()))

// Stripe Checkout link:
link, err := client.Orders.CreatePaymentLink(ctx, orderID,
  partnerapi.PaymentLinkCreateRequest{Amount: 250})
payment link201 Created
# POST /partner/v1/orders/{id}/payment-links {"amount": 250}
{
  "url": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3...",
  "sessionId": "cs_live_a1B2c3...",
  "expiresAt": "2026-07-14T15:00:00Z",
  "emailSent": true
}
Endpoint

Documents

GET /partner/v1/documents
GET /partner/v1/documents/{id}/download

File metadata for an entity (contract PDFs, work-order previews, verification photos) plus short-lived download links. Requires partner-api.documents.read.

entityType
string · required

order, quote, or workOrder.

entityId
string · required

The entity’s id.

The download endpoint returns a presigned URL that expires after ~10 minutes — fetch it promptly, don’t store it. Internal storage paths are never exposed.

request
curl "__BASE__/partner/v1/documents?entityType=order&entityId=665f..." \
  -H "Authorization: Bearer <token>"

# then fetch a short-lived link:
curl "__BASE__/partner/v1/documents/66aa11bb22cc33dd44ee55ff/download" \
  -H "Authorization: Bearer <token>"
const docs = await client.documents.list({
  entityType: "order", entityId: orderId,
});
const link = await client.documents.download(docs.data[0].id);
docs, err := client.Documents.List(ctx, partnerapi.DocumentListParams{
  EntityType: "order", EntityID: orderID,
})
link, err := client.Documents.Download(ctx, docs.Data[0].ID)
download response200 OK
{
  "downloadUrl": "https://...presigned...",
  "fileName": "contract-13847.pdf",
  "expiresAt": "2026-07-02T17:00:00Z"
}
Endpoint

Events feed

GET /partner/v1/events
POST /partner/v1/events/{id}/redeliver
GET /partner/v1/webhook-deliveries

A lossless, cursor-based change feed of everything that happens to your company’s data — from any source (portal, automation, Partner API). Requires partner-api.events.read. Events are retained ~90 days.

cursor
string

The last event id you processed; omit to start from the oldest retained event. Store it durably and resume after downtime — nothing is lost between polls.

types
string

Comma-separated types, e.g. order.status_changed,payment.paid.

limit
integer · default 100

Max events per page (max 200).

Event types

lead|quote|order|work_order .created/.updated/.status_changed/.deleted, customer.created/.updated/.deleted, contract.sent/.signed/.completed/.voided, payment.created/.paid/.failed/.refunded, quote.sent/.expired, order.cancelled, delivery.scheduled/.dispatched/.delivered, customer.merged, document.created. Deletes are tombstones carrying only the resource type/id. data is a compact snapshot; fetch resourceUrl for the full DTO. There are no inventory.* events — a unit’s availability changes through its work order and linked quote/order, so subscribe to those and poll lot-stock to reconcile.

request
curl "__BASE__/partner/v1/events?types=order.status_changed,payment.paid&cursor=66cc..." \
  -H "Authorization: Bearer <token>"
const page = await client.events.list({
  types: ["order.status_changed", "payment.paid"],
  cursor: lastCursor,
});
// store page.nextCursor durably and pass it on the next poll
page, err := client.Events.List(ctx, partnerapi.EventListParams{
  Types:  []string{"order.status_changed", "payment.paid"},
  Cursor: lastCursor,
})
// or stream every event with automatic pagination:
err = client.Events.Each(ctx, partnerapi.EventListParams{}, func(e partnerapi.EventItem) {
  // handle event
})
feed response200 OK
{
  "data": [
    {
      "id": "66cc33dd44ee55ff66778899",
      "type": "order.status_changed",
      "occurredAt": "2026-07-01T18:22:04Z",
      "resourceType": "order",
      "resourceId": "665f0a1b2c3d4e5f60718293",
      "resourceUrl": "/partner/v1/orders/665f...",
      "externalReferences": { "crmDealId": "D-99812" },
      "data": { "status": "Unprocessed" }
    }
  ],
  "nextCursor": "66cc33dd44ee55ff66778899",
  "hasMore": false
}
Concepts

Webhooks

Push delivery of the same events as the feed. Manage endpoints in the portal under Settings → Company → Developer API → Webhooks; the signing secret is shown once at create/rotate time.

Delivery contract

X-ShedCloud-Signature

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<body>">

X-ShedCloud-Event-Id

Dedupe key — delivery is at-least-once.

X-ShedCloud-Event-Type

Event type without parsing the body.

Respond 2xx within 10 seconds; failures retry with backoff for ~24h, then dead-letter. Verify the signature (reject timestamps older than ~5 minutes), and reconcile via the events feed when in doubt. Every official SDK ships a verification helper — prefer those over hand-rolling HMAC.

verify a delivery
// No SDK — Node crypto directly:
import crypto from "node:crypto";

function verify(secret, header, rawBody) {
  const p = Object.fromEntries(header.split(",").map(x => x.split("=")));
  const expected = crypto.createHmac("sha256", secret)
    .update(`${p.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(p.t)) < 300;
  return fresh && crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(p.v1));
}
import { verifyWebhookSignature } from "@shedcloud/partner-api";

// Pass the RAW request body — don't re-serialize parsed JSON.
app.post("/webhooks/shedcloud", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    await verifyWebhookSignature(req.body, req.header("X-ShedCloud-Signature") ?? "", secret);
  } catch {
    return res.status(400).send("bad signature");
  }
  res.sendStatus(200); // ack fast, process async
});
// Pass the RAW request body — don't re-serialize parsed JSON.
func handleWebhook(w http.ResponseWriter, r *http.Request) {
  body, _ := io.ReadAll(r.Body)
  err := partnerapi.VerifyWebhookSignature(
    secret, r.Header.Get("X-ShedCloud-Signature"), body, 5*time.Minute)
  if err != nil {
    http.Error(w, "bad signature", http.StatusBadRequest)
    return
  }
  w.WriteHeader(http.StatusOK) // ack fast, process async
}
from shedcloud_partner_api import verify_webhook_signature

# Pass the RAW request body — don't re-serialize parsed JSON.
def handle_webhook(request):
    verify_webhook_signature(
        request.body, request.headers["X-ShedCloud-Signature"], secret
    )
    return Response(status=200)  # ack fast, process async
use ShedCloud\PartnerApi\Webhooks;

$body = file_get_contents('php://input');
Webhooks::verifySignature(
    getenv('SHEDCLOUD_WEBHOOK_SECRET'),
    $_SERVER['HTTP_X_SHEDCLOUD_SIGNATURE'] ?? '',
    $body,
);
http_response_code(200); // ack fast, process async
body = request.body.read
ShedCloud::PartnerApi::Webhooks.verify_signature(
  ENV.fetch('SHEDCLOUD_WEBHOOK_SECRET'),
  request.headers['X-ShedCloud-Signature'],
  body,
)
head :ok # ack fast, process async
Endpoint

Configurator sessions

POST /partner/v1/configurator-sessions

Mints a single-use launch URL into ShedCloud’s Structura 3D configurator — optionally pre-attached to a customer, an existing quote’s design (quoteId), or an in-stock unit (workOrderId / serialNumber). Requires partner-api.configurator-sessions.write; supports Idempotency-Key.

customerId
string

Attach an existing customer (create one first via POST /partner/v1/customers).

quoteId · workOrderId · serialNumber
string

Design context — at most one.

returnUrl
string

Where to send the shopper after saving. Its origin must be allowlisted in Settings → Developer API → Configurator Launch Allowlist. The return redirect carries only psid + quoteId — no PII.

ttlSeconds
integer · default 900

Link lifetime, 60–3600 seconds.

Opening launchUrl consumes the token (reused/expired links get 410 Gone) and 302-redirects into the configurator. The authoritative “design saved” signal is the quote.created / quote.updated event on your webhook/feed.

request
curl -X POST "__BASE__/partner/v1/configurator-sessions" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "665f0a1b2c3d4e5f60718200",
    "returnUrl": "https://your-crm.example.com/deals/99812"
  }'
const session = await client.configuratorSessions.create({
  customerId: "665f0a1b2c3d4e5f60718200",
  returnUrl: "https://your-crm.example.com/deals/99812",
});
session, err := client.ConfiguratorSessions.Create(ctx,
  partnerapi.ConfiguratorSessionCreateRequest{
    CustomerID: "665f0a1b2c3d4e5f60718200",
    ReturnURL:  "https://your-crm.example.com/deals/99812",
  })
response201 Created
{
  "sessionId": "66dd44ee55ff667788990011",
  "launchUrl": "__BASE__/api/external/partner/launch?token=plt_...",
  "expiresAt": "2026-07-11T22:33:00Z",
  "customerId": "665f0a1b2c3d4e5f60718200"
}
Endpoint

Updating resources

PATCH /partner/v1/{resource}/{id}
POST /partner/v1/{resource}/{id}/status

Both writes require the resource’s .write scope, are attributed to your credential in the audit trail, invalidate the resource’s list cache, and return the updated item.

PATCH — allowlisted fields

The body is a flat JSON object. Unknown or forbidden keys reject the whole request with 400 and the list of allowed fields.

Leads

salesLocation (location id), salespersonName, salespersonEmail.

Quotes / Orders

customerName, customerEmail, customerPhone, salespersonName, salespersonEmail, salesLocation, deliveryAddress, deliveryCity, deliveryState, deliveryZipCode. Quotes additionally accept validUntil (RFC 3339 or YYYY-MM-DD; null clears it).

Work orders

title, description, buildingLocation (location id), promisedDate.

Locations

See Locations — full create field set.

Customers

See Customers — contact fields plus active.

Status transitions

Only the transitions below are allowed; anything else returns 409 Conflict. Processed can never be set via the Partner API — it creates work orders and fires tax/CRM/email side effects, so it stays internal-only in v1.

Leads

Open → Lost / Retired · Lost → Open · Retired → Open

Quotes

Open → Active / Lost / Retired · Active → Lost / Retired · Lost → Active · Retired → Active

Orders

Unsubmitted → Unprocessed / On hold · Unprocessed → On hold · On hold → Unprocessed

Work orders

Open build → Build in process → Finished good → Delivered

No pricing, serial-number, or work-order-linking fields are writable in v1. Cross-company ids always return 404.
patch request
curl -X PATCH "__BASE__/partner/v1/orders/665f...8293" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "customerPhone": "555-0199" }'
const order = await client.orders.update(orderId, {
  customerPhone: "555-0199",
});
order, err := client.Orders.Update(ctx, orderID,
  partnerapi.OrderPatchRequest{CustomerPhone: "555-0199"})
status request
curl -X POST "__BASE__/partner/v1/work-orders/665f...8294/status" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "status": "Finished good", "actionDescription": "Build crew A" }'
await client.workOrders.updateStatus(workOrderId, {
  status: "Finished good",
  actionDescription: "Build crew A",
});
workOrder, err := client.WorkOrders.UpdateStatus(ctx, workOrderID,
  partnerapi.StatusUpdateRequest{
    Status:            "Finished good",
    ActionDescription: "Build crew A",
  })
blocked transition409 Conflict
{
  "error": "transition from \"Unprocessed\" to \"Processed\" is not allowed through the Partner API"
}
Concepts

External references

Attach your own correlation ids (CRM deal id, ERP order id, …) to leads, quotes, orders, work orders, and customers. They’re echoed in every DTO and every event, and filterable on lists with ?externalRef=key:value — no local id-mapping table needed.

Set at create

Every create body accepts an externalReferences object.

Merge via PATCH

Keys are merged into the existing map; a null value deletes that key.

Limits

Max 10 keys per record · keys match [A-Za-z0-9_-]{1,64} · values 1–128 chars.

merge / delete
curl -X PATCH "__BASE__/partner/v1/orders/665f...8293" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "externalReferences": { "crmDealId": "D-99812", "oldKey": null } }'
await client.orders.update(orderId, {
  externalReferences: { crmDealId: "D-99812", oldKey: null },
});
dealID := "D-99812"
order, err := client.Orders.Update(ctx, orderID, partnerapi.OrderPatchRequest{
  ExternalRefs: partnerapi.ExternalReferencesPatch{
    "crmDealId": &dealID,
    "oldKey":    nil, // null deletes the key
  },
})
Concepts

Versioning & If-Match

Sales entities, work orders, and customers carry an integer version incremented on every write from any source. Get-one responses mirror it into a strong ETag header.

Send If-Match: "<version>" on a PATCH to make it conditional: on mismatch you get 409 Conflict with the current version in the ETag header — re-read, re-apply, retry. Omitting If-Match keeps last-write-wins behavior.

conditional patch
curl -X PATCH "__BASE__/partner/v1/orders/665f...8293" \
  -H "Authorization: Bearer <token>" \
  -H 'If-Match: "4"' \
  -H "Content-Type: application/json" \
  -d '{ "customerPhone": "555-0199" }'
const order = await client.orders.update(
  orderId,
  { customerPhone: "555-0199" },
  { ifMatch: 4 },
);
order, err := client.Orders.Update(ctx, orderID,
  partnerapi.OrderPatchRequest{CustomerPhone: "555-0199"},
  partnerapi.WithIfMatch(4))
stale version409 Conflict
ETag: "5"

{ "error": "version mismatch: the record is at version 5, not 4 — re-read it and retry with the current version" }
Concepts

Idempotency keys

Every create endpoint (POST /partner/v1/quotes, /quotes/{id}/convert, /leads, /locations, /customers) accepts an optional Idempotency-Key header — an opaque string of your choosing (a UUID is typical), max 255 characters.

When a keyed request succeeds, the response is stored for 24 hours. Retrying the same key within that window (e.g. after a network timeout where you never saw the response) replays the stored response instead of creating a duplicate. Replayed responses carry an Idempotency-Replayed: true header.

Same key, same body

Stored response replayed with Idempotency-Replayed: true.

Same key, different body

422 Unprocessable Entity.

Same key, original still running

409 Conflict — retry shortly.

Original failed with 5xx

Key is released; the retry executes normally.

Keys are scoped per company and per endpoint, so the same key value on two different endpoints never collides. Requests without the header behave normally (non-idempotent).

request
curl -X POST "__BASE__/partner/v1/quotes" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0d3bd9b2-6a5e-4c1f-9f6a-1c2d3e4f5a6b" \
  -d '{ "serialNumber": "SC-2024-00123", "customer": { "email": "[email protected]" } }'
const quote = await client.quotes.create(
  { serialNumber: "SC-2024-00123", customer: { email: "[email protected]" } },
  { idempotencyKey: "0d3bd9b2-6a5e-4c1f-9f6a-1c2d3e4f5a6b" },
);
quote, err := client.Quotes.Create(ctx, partnerapi.QuoteCreateRequest{
  SerialNumber: "SC-2024-00123",
  Customer:     partnerapi.QuoteCreateCustomer{Email: "[email protected]"},
}, partnerapi.WithIdempotencyKey("0d3bd9b2-6a5e-4c1f-9f6a-1c2d3e4f5a6b"))
retried response headers201 Created
Content-Type: application/json
Idempotency-Replayed: true
Concepts

Versioning policy

The API version is carried in the path: /partner/v1. Within v1 the contract is additive-only:

We may add

New endpoints, new optional request fields and query parameters, new response fields, new event types, and new enum values. Your integration must tolerate unknown response fields.

We never

Rename, retype, or remove an existing response field; change the meaning of an existing parameter; or tighten validation in a way that rejects previously valid requests.

Breaking changes

Would ship as a new path prefix (/partner/v2) with a long overlap window. There is no v2 planned.

The official SDKs follow semver in lockstep with API additions across Node, Go, Python, PHP, and Ruby: new endpoints/fields arrive as minor SDK releases, fixes as patches. Each changelog entry notes the first SDK version that supports it. Pinning an older SDK version is always safe — it simply won’t surface newer fields.

Not to be confused with record versioning & If-Match, which is per-record optimistic concurrency.

forward-compatible parsing
# Good: ignore fields you don't know.
# A response that gains a new field, e.g.
{
  "id": "665f...8293",
  "status": "Open",
  "aFieldAddedNextMonth": "..."
}
# must keep parsing in your integration.
Reference

Changelog

What changed in the Partner API, newest first. The API is additive-only within v1 (see the versioning policy); SDK versions noted per entry are the first release supporting the change.

2026-07-13

  • B2B agreements (read-only)GET /partner/v1/agreements, /agreements/{id}, /agreements/active, and /agreements/{id}/state-legal[/{state}]: sanitized dealer ↔ RTO / ERP / DMAN partnership records and per-state RTO legal configuration. Scope partner-api.agreements.read. RTO quotes/orders now include rto.agreementId.
  • Per-endpoint SDK examples — every request example on this page has cURL / Node / Go / Python / PHP / Ruby tabs (Stripe-style); your language choice is remembered across the whole page.
  • Official SDKs — Python, PHP, and Ruby — first-party clients on PyPI, Composer, and RubyGems with full parity to Node and Go.
  • Machine/AI-readable docs — the documentation is now published in machine-friendly formats at stable public paths: /llms.txt (llms.txt index for AI agents), /partner/reference.md (full reference as Markdown), /partner/openapi.json / .yaml (raw OpenAPI spec), and /partner/changelog.md.
  • User writesPOST /partner/v1/users creates a company user through the portal's own pipeline (profile, RBAC role, locations, invitation email); existing accounts are linked by email instead of duplicated, and the login is created at the user's first sign-in via the invite. PATCH /partner/v1/users/{id} updates profile/role/locations/active (owner protected). GET /partner/v1/roles lists assignable roles. New scope partner-api.users.write. (npm 0.5.0, Go v0.5.0)
  • Payment writesPOST /partner/v1/orders/{id}/payments records a manual payment (cash | check | financed | manual) through the portal's own pipeline (ledger recalc, audit, payment.created event); POST /partner/v1/orders/{id}/payment-links creates a Stripe Checkout session and returns the hosted payment URL. New scope partner-api.payments.write. (npm 0.5.0, Go v0.5.0)
  • Work order creationPOST /partner/v1/work-orders: portal building-wizard parity with friendly purchaseType / workOrderType / deliveryType enums, optional sizeId product attach, unique serial enforcement. Scope partner-api.work-orders.write. (npm 0.5.0, Go v0.5.0)
  • Order creationPOST /partner/v1/orders: full order creation with customer (id or inline), location, base product + optional size, upgrade lines, pricing header, delivery address, and an optional configurator payload. New orders start in Unsubmitted. Scope partner-api.orders.write. (npm 0.5.0, Go v0.5.0)
  • Line-item writesPOST /partner/v1/{quotes|orders}/{id}/line-items (idempotent on lineKey) and DELETE .../line-items/{lineId}; the base product line is protected. (npm 0.5.0, Go v0.5.0)
  • Product writesPOST /partner/v1/products (optional lineId line linking), PATCH /partner/v1/products/{id}, and POST /partner/v1/products/{id}/sizes (size children with real width/length/price). Scope partner-api.products.write. (npm 0.5.0, Go v0.5.0)
  • DomainsGET /partner/v1/domains and GET /partner/v1/locations/{id}/domains: white-label storefront subdomains with per-location assignments and the new defaultForStore flag (filterable). Scope partner-api.domains.read. (npm 0.5.0, Go v0.5.0)
  • Default For Store — locations can mark one domain as their primary storefront from the portal's Domains tab; the server enforces at most one default per location.
  • Changelog & versioning policy published — this section, the documented additive-only policy, and official SDK quickstarts on this page.

2026-07-12

  • API key scope management — edit the scopes of an existing credential from Settings → Company → Developer API without rotating the secret.

2026-07-11

  • Events feed & webhooksGET /partner/v1/events, signed webhook subscriptions, delivery inspection, and manual redelivery. Scope partner-api.events.read.
  • External references — attach your own ids to sales entities and work orders via PATCH (merge semantics, null deletes).
  • Optimistic concurrencyETag on get-one, If-Match on PATCH.
  • New read resources — payments, documents (+ download), users, stock templates, order contract PDF, status history and line items.
  • Configurator sessions — single-use launch URLs into the 3D configurator.
  • Customer mergePOST /partner/v1/customers/{id}/merge.

2026-07-10

  • Lot stock exterior attributesattributes (siding, siding color, trim color, roof material, roof color) on each unit. (npm 0.3.0, Go v0.3.0)
  • Lot stock defaults to ALL — no implicit Lot Stock filter; pass purchaseType to filter.
  • Products: finished only + images — raw materials and kits excluded; images carries public gallery URLs.
  • Lead creationPOST /partner/v1/leads with automatic salesperson routing.
  • Quote → order conversionPOST /partner/v1/quotes/{id}/convert.
  • Idempotency keysIdempotency-Key on all create endpoints (24h replay window).
  • Store hoursstoreHours on locations.

2026-07-09

  • Initial public release — company-scoped credentials (API keys + OAuth2), scoped authorization, rate limiting, caching.
  • Launch resources — lot stock, leads, quotes (+ in-stock create), orders, work orders, locations, customers, products.
  • Official SDKs@shedcloud/partner-api (npm) and shedcloud-gomod/partnerapi (Go).