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.
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.
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.
# 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.
# 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.
Limits & timeouts
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": "invalid or expired credential" }{ "error": "credential is not authorized for this scope" }{ "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",
"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.location-budgets.read",
"partner-api.location-budgets.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.site-events.read",
"partner-api.site-events.write",
"partner-api.configurator-sessions.write",
"partner-api.domains.read"
]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+.
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.
npm install @shedcloud/partner-apigo get github.com/Corland-Partners-LLC/shedcloud-gomod/partnerapipip install shedcloud-partner-apicomposer require shedcloud/partner-apigem install shedcloud-partner_apiimport { 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',
)Issue an access 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
Must be client_credentials.
OAuth client id. Omit if using HTTP Basic auth.
OAuth client secret. Omit if using HTTP Basic auth.
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"),
},
}){
"access_token": "sc_at_xxxxxxxxxxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "partner-api.lot-stock.read"
}Credential introspection
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.
The company this credential is permanently bound to.
api_key or oauth_client.
Scopes granted to this credential.
requestsPerSecond and burst — see Rate limits.
curl "__BASE__/partner/v1/me" \
-H "Authorization: Bearer <token>"const me = await client.me();me, err := client.Me(ctx){
"companyId": "6529b409e7d0f84e18e2a001",
"companyName": "Leland's Sheds",
"credentialType": "api_key",
"scopes": ["partner-api.lot-stock.read", "partner-api.quotes.write"],
"rateLimit": { "requestsPerSecond": 20, "burst": 40 }
}Company profile
Returns the authenticated company’s profile from Settings → Company: name, contact info, address, timezone, logo URL, company type, and sell types. Requires the partner-api.company.read scope.
The company id (same value as companyId from /me).
Primary company contact fields.
Company mailing address.
Company-level IANA timezone (also used on locations).
Public CDN URL for the company logo, when configured.
Company type label (e.g. DEALER).
Sell types enabled for the company.
curl "__BASE__/partner/v1/company" \
-H "Authorization: Bearer <token>"const company = await client.company.get();company, err := client.Company.Get(ctx){
"id": "6529b409e7d0f84e18e2a001",
"name": "Leland's Sheds",
"email": "[email protected]",
"phone": "817-555-0100",
"address": "123 Main St",
"city": "Abilene",
"state": "TX",
"zipCode": "79601",
"country": "US",
"timezone": "America/Chicago",
"logoUrl": "https://cdn.shedcloud.com/companies/logo.png",
"companyType": "DEALER",
"sellTypes": ["RTO", "Cash"]
}List on-lot inventory
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 number (1-based).
Page size, maximum 100.
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.
Filter by location id or slug.
Filter by the locations’ exact region label; combines with location (both must match).
Siding product id (ObjectId) or case-insensitive substring of the siding display name.
Case-insensitive substring match on serial number or title.
Sort by serialNumber, title, price, or createdAt.
Sort direction: asc or desc.
When true, include units that would otherwise be hidden after the store-expiration grace period. sold still reflects Processed linkage.
By default, sellable units are returned and collapsed to one row per work order. Units linked to a Processed sales order stay on the feed for a configurable grace period (default 30 days from the Processed date) unless showWhenExpired is true on the unit. Pass ignoreStoreExpiration=true to include units past that window. The internal portal stock screen still hides Processed-linked units immediately.
Response fields
Inventory items (see example).
true when the unit is linked to a Processed sales order (may still appear during the grace period).
Grace period after Processed before hiding from this feed (default 30 when unset). Configurable per unit in the portal inventory stock card.
When true, the unit stays on the Partner feed after expiration.
RFC 3339 timestamp when the linked order reached Processed (omitted until set).
Public CDN URLs for uploaded work-order Hero_Images / Product_Images (empty array when none).
Public CDN URLs for uploaded work-order Front_Images (empty array when none).
Public CDN URLs for uploaded work-order Back_Images (empty array when none).
Public CDN URLs for uploaded work-order Left_Images (empty array when none).
Public CDN URLs for uploaded work-order Right_Images (empty array when none).
Public CDN URLs for uploaded work-order Exterior_Images (empty array when none).
Public CDN URLs for uploaded work-order Interior_Images (empty array when none).
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.
Selling price after any inventory card discount (building subtotal minus discount when set).
Inventory card discount amount set on the unit in the portal (omitted when unset).
Inventory card discount reason (omitted when unset). Valid discounts require amount > 0 and a reason of at least 3 characters.
Echoed pagination values.
Total matching records across all pages.
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",
}){
"data": [
{
"id": "6529b409e7d0f84e18e2a108",
"workOrderId": "6529b409e7d0f84e18e2a222",
"serialNumber": "SC-2024-00123",
"title": "10x16 Lofted Barn",
"purchaseType": "Lot Stock",
"basePrice": 8200.00,
"price": 8745.00,
"discount": 250.00,
"discountReason": "Floor model",
"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,
"storeExpirationDays": 30,
"showWhenExpired": false,
"storeExpirationAnchorAt": "2026-07-01T18:22:04Z"
}
],
"page": 1,
"limit": 50,
"total": 137
}Stock templates
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
Pagination — limit max 60, default 50.
Case-insensitive match on work-order number, template name, or product name.
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.
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},
}){
"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
}Leads
Lists and reads the company’s leads. Requires partner-api.leads.read.
Query parameters
Pagination — limit max 100, default 50.
Substring match on customer name, order number, email, or phone.
orderNumber, customerName, status, total, createdAt (default), updatedAt · asc/desc.
e.g. Open, Lost, Retired (case-insensitive).
Location id or slug.
Targeted field filters.
RFC 3339 or YYYY-MM-DD date ranges.
Lead items include optional salesSource and sourceMetadata when set — see Sales source & metadata.
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"){
"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" },
"salesSource": "Website",
"sourceMetadata": { "utm_campaign": "spring-sale", "landingPath": "/inventory/dallas" },
"createdAt": "2026-06-28T15:03:11Z",
"updatedAt": "2026-07-01T18:22:04Z"
}
],
"page": 1, "limit": 25, "total": 8
}Create a lead
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
Sales location id; must be one of your locations (404 otherwise).
{ "name", "email", "phone" } — at least one of the three is required.
Explicit salesperson; skips lead routing.
Explicit salesperson email; skips lead routing.
Portal Contact tab Source value (e.g. Website, Facebook). Omitted = no-op. When set, also syncs to the linked customer. See allowed values.
Arbitrary JSON key/value attribution alongside salesSource (site path, UTM params, …). Does not sync to customer.
Returns 201 with the created lead (same shape as GET /partner/v1/leads/{id}).
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" },
"salesSource": "Website",
"sourceMetadata": { "utm_campaign": "spring-sale" }
}'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",
},
}){
"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" }
}Quotes
Lists and reads the company’s quotes. Requires partner-api.quotes.read. Accepts every lead-list parameter plus:
true = only quotes converted to an order; false = only unconverted.
Quote items add pricing (subtotal, total, monthly payment, payment type, and when linked from In Stock with a card discount — discount / discountReason), serialNumber, workOrderId, conversion linkage (converted, convertedOrderId, convertedOrderNumber), and optional salesSource / sourceMetadata — see Sales source & metadata.
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.
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){
"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", "discount": 250.00, "discountReason": "Floor model" },
"serialNumber": "SC-2024-00123",
"converted": false
}Create a quote from an in-stock unit
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). When the unit has a valid inventory card discount (discount / discountReason on lot-stock), the payment discount and reason are copied onto the quote automatically. 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
Serial number of the in-stock unit. Required unless workOrderId is sent.
Work order id of the unit; takes precedence when both are sent.
{ "name", "email", "phone" } — an email, or a name + phone, is required. Matched by email when the customer already exists.
{ "address", "city", "state", "zipCode", "latitude", "longitude" } — stamped onto the quote and cascaded to the linked work order.
Sales location id. Defaults to the unit’s own location.
Lot Stock, Rental Return, or Immediate Sale; inferred from the unit when omitted.
Sell price; the unit’s own pricing is used when omitted.
Free-form note stored on the quote.
Portal Contact tab Source value. Omitted = no-op. When set, also syncs to the linked customer. See allowed values.
Arbitrary JSON key/value attribution alongside salesSource. Does not sync to customer.
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.
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",
},
}){
"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
}Convert a quote to a sales order
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}/status → Unprocessed when ready.
Body fields (optional)
Overrides the salesperson copied from the quote.
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.
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{}){
"id": "665f0a1b2c3d4e5f60718296",
"orderNumber": 13849,
"status": "Unsubmitted",
"customer": { "name": "Jane Doe", "email": "[email protected]" },
"pricing": { "subtotal": 8200.00, "total": 8995.00, "paymentType": "rto", "discount": 250.00, "discountReason": "Floor model" },
"serialNumber": "SC-2024-00123",
"workOrderId": "665f0a1b2c3d4e5f60718294",
"sourceQuoteId": "665f0a1b2c3d4e5f60718293",
"sourceQuoteNumber": 13848
}Sales orders
Lists and reads the company’s sales orders. Requires partner-api.orders.read. Accepts every lead-list parameter plus:
rto or cash.
Match on the unit serial number.
Sale / sold-date range (RFC 3339 or YYYY-MM-DD). Stamped when the order enters the company Sold-by Status, or set via the portal Sale Date picker / Partner PATCH.
Order items add pricing (including discount / discountReason when linked from In Stock with a card discount, and changeOrderFee when a change-order fee was applied — change orders are a fee plus payment-ledger entries, not a separate order entity), optional saleDate and soldPricing (a frozen copy of base price, upgrades, delivery, tax, subtotal, and total taken at Sold-by Status entry — independent of live pricing), deposits, serialNumber, workOrderId, source-quote linkage (sourceQuoteId, sourceQuoteNumber), delivery dates (expectedDeliveryDate, deliveredAt), and optional salesSource / sourceMetadata — see Sales source & metadata.
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, optional salesSource / sourceMetadata, 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.
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",
}){
"id": "665f0a1b2c3d4e5f60718293",
"orderNumber": 13847,
"status": "Unprocessed",
"pricing": { "subtotal": 8200.00, "total": 8995.00, "paymentType": "rto", "discount": 250.00, "discountReason": "Floor model" },
"saleDate": "2026-07-01T00:00:00Z",
"soldPricing": {
"basePrice": 7500.00,
"upgradesAmount": 700.00,
"deliveryFee": 250.00,
"taxAmount": 545.00,
"subtotal": 8200.00,
"total": 8995.00
},
"serialNumber": "SC-2024-00123",
"workOrderId": "665f0a1b2c3d4e5f60718294",
"sourceQuoteId": "665f0a1b2c3d4e5f60718295"
}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"))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)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
Same as the sales lists.
Substring match on serial number, title, or work order number.
workOrderNumber, serialNumber, status, createdAt, updatedAt.
e.g. Open build, Build in process, Finished good, Delivered.
Targeted filters; linkedOrderId is the exact linked sales order id.
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:
new-build · existing-physical-inventory · general · stock.
made-to-order · lot-stock · rental-return · immediate-sale · template.
delivery-from-factory · built-on-site · delivered-from-dealer · delivered-from-lot-stock.
Optional product size to attach — also creates the manufacturing BOM unless purchaseType is existing-physical-inventory.
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.
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",
}){
"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"
}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"))Locations
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
Same as the sales lists.
Substring match on name, code, or city.
name, code, city, createdAt, updatedAt.
Flag filters.
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; quotes and orders at those locations are also excluded from list/get/patch/status.
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.
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"}){
"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 }
}
}Location sales budgets
Per-location sales targets (annual and monthly dollar amounts, distribution percentages, lead targets, bonus thresholds) — the same records the portal manages at GET/POST/PUT /v1/budget/sales. One budget per company, location, and calendar year. Reads require partner-api.location-budgets.read; writes require partner-api.location-budgets.write.
List query parameters
Standard pagination (default limit 25, max 100).
Filter to a calendar year.
Filter to one sales location (24-char hex id).
Create / update body
POST and full-replace PUT accept the same field set. Required: locationId, year (2000–2100). Optional monthly dollar targets (january…december), monthly distribution percentages (januaryPct…decemberPct), annualBudget, locationPct, bonus thresholds, inventory cap, close-rate target, and monthly lead targets. The server derives read-only weekly amounts (januaryWeek…) as monthly ÷ 4.3 on write.
locationId must reference a location visible to the Partner API (hidden locations return 404). Duplicate company/location/year returns 409 on create. Create supports idempotency keys.
curl -X POST "__BASE__/partner/v1/location-budgets" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"locationId": "66c00443c2d8aa83c5757dcf",
"year": 2026,
"annualBudget": 1200000,
"locationPct": 12.5,
"january": 85000,
"february": 90000,
"march": 95000
}'{
"id": "66d1a2b3c4d5e6f708192a3",
"locationId": "66c00443c2d8aa83c5757dcf",
"year": 2026,
"annualBudget": 1200000,
"locationPct": 12.5,
"january": 85000,
"february": 90000,
"januaryWeek": 19767.44,
"bonusThreshold1": 250000,
"inventoryCap": 45,
"closeRateTarget": 0.35,
"januaryLeadTarget": 120,
"createdAt": "2026-07-25T18:00:00Z",
"updatedAt": "2026-07-25T18:00:00Z"
}Customers
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
Same as the sales lists.
Substring match on name, email, or phone.
Targeted substring filters.
name, email, createdAt, updatedAt.
RFC 3339 or YYYY-MM-DD date ranges.
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.
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}){
"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"
}Products
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
Same as the sales lists.
Substring match on name, SKU, or description.
Filter by SKU (substring).
Filter by active flag.
name, sku, price, createdAt, updatedAt.
RFC 3339 or YYYY-MM-DD date ranges.
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"}){
"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"
]
}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
Pagination — domains are a small set, paged in memory.
(list only) Narrow to domains assigned to this location.
true → only domains flagged Default For Store for their location(s).
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{}){
"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"]
}
]
}
]
}B2B agreements
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)
Pagination — limit max 100.
Filter by status (active, pending, revoked, …).
Substring match on counterparty name.
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"){
"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
}Users / salespeople
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.
Pagination — limit max 100.
Substring match on name or email.
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.
Profile. The email doubles as the username.
Company RBAC role id — discover via GET /partner/v1/roles.
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).
{
"id": "66a0aa0bb1cc2dd3ee4ff556",
"name": "John Rep",
"email": "[email protected]",
"phone": "555-0100",
"active": true,
"locationIds": ["66c00443c2d8aa83c5757dcf"],
"allLocations": false,
"inLeadRoutingPool": true
}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})// GET /partner/v1/roles
{
"id": "665f0a1b2c3d4e5f60718293",
"name": "Salesperson",
"key": "salesperson",
"isSystem": true
}Status history, line items & 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.
{
"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
}{
"orderId": "665f0a1b2c3d4e5f60718293",
"status": "completed",
"customerSigned": true,
"customerSignedAt": "2026-07-02T16:40:00Z",
"salespersonSigned": true,
"signedPdfDocumentId": "66aa11bb22cc33dd44ee55ff"
}Payments
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.
Filter by sales order id.
Case-insensitive exact match, e.g. paid, Refunded.
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.
cash, check, financed, or manual. card/ach are rejected — those records are created by the Stripe pipeline (use payment links).
Payment amount (> 0).
Defaults to "<Method> payment".
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 to collect (minimum 1).
Checkout line-item label (defaults to "Order #<n>").
Recipient override; defaults to the order’s customer email.
Email the link to the customer.
ISO currency code.
{
"id": "66bb22cc33dd44ee55ff6677",
"orderId": "665f0a1b2c3d4e5f60718293",
"amount": 1314.82,
"method": "card",
"status": "paid",
"providerReference": "cs_live_a1B2c3...",
"createdAt": "2026-07-01T18:25:11Z"
}# 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})# 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
}Sales ledger
Delta-based sales-order ledger movements — the same rows as the portal Sales Ledger report. Requires partner-api.sales-ledger.read.
saleDate is the calendar sale day for store / month reporting. ledgerDate is the status/event register and is independent of saleDate. cancelled is sticky when a previously sold order later moves to Cancelled or Deleted.
Filter by sale date (RFC 3339 or YYYY-MM-DD). Prefer this for monthly store totals.
Filter by status/event date register.
true / false. Sticky flag after Cancelled/Deleted on a previously sold order.
Exact match, e.g. Unprocessed.
e.g. initial, status_change, amount_change.
24-char hex ObjectIds.
{
"id": "66bb22cc33dd44ee55ff6677",
"orderId": "665f0a1b2c3d4e5f60718293",
"orderNumber": 5095,
"customerName": "Jane Doe",
"locationName": "Belton",
"saleDate": "2026-07-01",
"ledgerDate": "2026-07-01T18:25:11Z",
"cancelled": false,
"currentStatus": "Unprocessed",
"movementType": "initial",
"deltaAmount": 33376.0,
"newOrderAmount": 33376.0
}Revenue / forecast
Revenue/Forecast KPIs (Actual, AFD/Transferred, forecast next / +2) and Authorized/Delivery week buckets for a selected Period. Requires partner-api.revenue-forecast.read.
Qualification is delivery / promised dates. Money is the Sales Ledger order amount (basePrice + upgrades − removedUpgrades) so a related order’s amount matches sales ledger newOrderAmount. Period totals do not equal the Sales Ledger report for the same calendar window.
this_week | last_week | this_month | last_month. Required unless from+to.
Inclusive custom range (YYYY-MM-DD). Forecast next / +2 shift by the same length.
Optional sales location id (24-char hex).
true / 1 attaches WO + related order rows. Capped at 2000; detailsTruncated when hit.
Comma subset: actual, afdTransferred, forecastNextPeriod, forecastPlus2, authorized, weeks.
Weeks are Sunday–Saturday in the company timezone. Hide-from-Partner and ignore-on-ledger locations, and cancelled orders, are excluded. Delivery / tax / mileage are not included in amount.
curl "__BASE__/partner/v1/revenue-forecast?period=last_month&details=true&detailsFor=actual" \
-H "Authorization: Bearer <token>"const report = await client.revenueForecast.get({
period: "last_month",
details: true,
detailsFor: "actual",
});report, err := client.RevenueForecast.Get(ctx, partnerapi.RevenueForecastParams{
Period: "last_month",
Details: true,
DetailsFor: "actual",
}){
"period": { "key": "last_month", "start": "2026-07-01", "end": "2026-07-31", "timezone": "America/Chicago" },
"revenueForecast": {
"actual": { "amount": 82680, "units": 12 },
"afdTransferred": { "amount": 114849, "units": 8 }
},
"authorizedDelivery": {
"authorized": { "amount": 114849, "units": 8 },
"weeks": [
{ "key": "wk1", "start": "2026-08-23", "end": "2026-08-29", "amount": 114849, "units": 8 }
]
}
}Documents
File metadata for an entity (contract PDFs, work-order previews, verification photos) plus short-lived download links. Requires partner-api.documents.read.
order, quote, or workOrder.
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.
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){
"downloadUrl": "https://...presigned...",
"fileName": "contract-13847.pdf",
"expiresAt": "2026-07-02T17:00:00Z"
}Events feed
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.
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.
Comma-separated types, e.g. order.status_changed,payment.paid.
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.
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 pollpage, 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
}){
"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
}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
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<body>">
Dedupe key — delivery is at-least-once.
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.
// 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 asyncuse 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 asyncbody = request.body.read
ShedCloud::PartnerApi::Webhooks.verify_signature(
ENV.fetch('SHEDCLOUD_WEBHOOK_SECRET'),
request.headers['X-ShedCloud-Signature'],
body,
)
head :ok # ack fast, process asyncSite events
Behavioral analytics for your own marketing sites. Where the events feed reports business changes, site events report what anonymous shoppers do — page views, CTA clicks, products browsed, filters applied — and flow into the same ShedCloud analytics pipeline as the 3D configurator tracker. When your marketing site and the configurator share a visitor_id, the shopper’s whole journey (browse → design → quote) stitches into one lifetime visitor profile visible in the portal’s Customer Journey dashboard.
Ingest requires partner-api.site-events.write; read-back requires partner-api.site-events.read. Server-side only — partner keys are secrets, so collect events in the browser, batch them, and forward from your backend (max 25 events per call, body ≤ 64 KB, rate limited per company).
Unlike the rest of /partner/v1, the ingest body uses snake_case keys — it shares its envelope with the configurator tracker. The read-back response is regular camelCase.
One visit — rotate per browser session.
The shopper’s permanent identity (a localStorage UUID). Strongly recommended — events without it cannot build a visitor profile. Pass the same id to the configurator via the ?scv= query param on your “design your own” links to stitch journeys across properties.
The marketing site’s hostname, e.g. lelandssheds.com.
Each item needs event_type; occurred_at (RFC3339), page, product_id, product_name, and a free-form payload object are optional context.
Event vocabulary
event_type must be on the server allowlist — session (session.start/resume/complete/abandoned), page (page.view/exit), interaction (cta.click, menu.click, product.view, image.view, filter.apply, modal.open/close), selection (product.select, color.select, size.considered, option.considered, upgrade.add/remove, payment.select), and identity (customer.profile, identity.resolved, lead.created, quote.created). Unknown types are rejected. Any company_id in the body is ignored — the credential is the tenant.
Read back
GET /partner/v1/site-events returns your company’s tracked events from both channels (source is marketing or configurator), newest first, cursor paginated. Raw events are retained ~90 days.
Opaque cursor from a previous response.
Max events per page (max 200).
Only events from one session.
Comma-separated event types to include.
RFC3339 bounds on occurredAt.
curl -X POST "__BASE__/partner/v1/site-events" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"session_id": "b7c2a1f0-4b6e-4c58-9f1d-2f9d1f4a7e21",
"visitor_id": "9a41d6c2-8f3b-4f7e-9a0c-5d2e8b1c6f43",
"site_host": "lelandssheds.com",
"events": [
{ "event_type": "page.view", "occurred_at": "2026-07-19T12:00:00Z", "page": "/sheds/lofted-barn" },
{ "event_type": "cta.click", "page": "/sheds/lofted-barn", "payload": { "cta": "design-your-own" } }
]
}'// Note: this body is snake_case on the wire (tracker envelope).
const res = await client.siteEvents.track({
session_id: "b7c2a1f0-...",
visitor_id: "9a41d6c2-...",
site_host: "lelandssheds.com",
events: [
{ event_type: "page.view", page: "/sheds/lofted-barn" },
{ event_type: "cta.click", page: "/sheds/lofted-barn", payload: { cta: "design-your-own" } },
],
});res, err := client.SiteEvents.Track(ctx, partnerapi.SiteEventsTrackRequest{
SessionID: "b7c2a1f0-...",
VisitorID: "9a41d6c2-...",
SiteHost: "lelandssheds.com",
Events: []partnerapi.SiteEventInput{
{EventType: "page.view", Page: "/sheds/lofted-barn"},
{EventType: "cta.click", Page: "/sheds/lofted-barn"},
},
})res = client.site_events.track({
"session_id": "b7c2a1f0-...",
"visitor_id": "9a41d6c2-...",
"site_host": "lelandssheds.com",
"events": [
{"event_type": "page.view", "page": "/sheds/lofted-barn"},
{"event_type": "cta.click", "page": "/sheds/lofted-barn"},
],
})$res = $client->siteEvents->track([
'session_id' => 'b7c2a1f0-...',
'visitor_id' => '9a41d6c2-...',
'site_host' => 'lelandssheds.com',
'events' => [
['event_type' => 'page.view', 'page' => '/sheds/lofted-barn'],
['event_type' => 'cta.click', 'page' => '/sheds/lofted-barn'],
],
]);res = client.site_events.track({
session_id: "b7c2a1f0-...",
visitor_id: "9a41d6c2-...",
site_host: "lelandssheds.com",
events: [
{ event_type: "page.view", page: "/sheds/lofted-barn" },
{ event_type: "cta.click", page: "/sheds/lofted-barn" },
],
}){
"success": true,
"accepted": 2
}GET __BASE__/partner/v1/site-events?sessionId=b7c2a1f0-...&types=page.view
{
"data": [
{
"eventId": "01J9ZK7Q0X8R2M4T6V8W0Y2A4C",
"eventType": "page.view",
"occurredAt": "2026-07-19T12:00:00Z",
"sessionId": "b7c2a1f0-...",
"visitorId": "9a41d6c2-...",
"source": "marketing",
"siteHost": "lelandssheds.com",
"page": "/sheds/lofted-barn"
}
],
"nextCursor": "eyJjb21wYW55Ijo...",
"hasMore": true
}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.
Attach an existing customer (create one first via POST /partner/v1/customers).
Design context — at most one.
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.
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.
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",
}){
"sessionId": "66dd44ee55ff667788990011",
"launchUrl": "__BASE__/api/external/partner/launch?token=plt_...",
"expiresAt": "2026-07-11T22:33:00Z",
"customerId": "665f0a1b2c3d4e5f60718200"
}Updating resources
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.
salesLocation (location id), salespersonName, salespersonEmail, salesSource, sourceMetadata.
customerName, customerEmail, customerPhone, salespersonName, salespersonEmail, salesLocation, deliveryAddress, deliveryCity, deliveryState, deliveryZipCode, salesSource, sourceMetadata. Quotes additionally accept validUntil (RFC 3339 or YYYY-MM-DD; null clears it). Orders additionally accept saleDate (UTC calendar day; null clears) and soldPricing (partial object matching GET; top-level null clears all sold amounts). Live pricing stays read-only.
title, description, buildingLocation (location id), promisedDate.
See Locations — full create field set.
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.
Open → Lost / Retired · Lost → Open · Retired → Open
Open → Active / Lost / Retired · Active → Lost / Retired · Lost → Active · Retired → Active
Unsubmitted → Unprocessed / On hold · Unprocessed → On hold · On hold → Unprocessed
Open build → Build in process → Finished good → Delivered
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"})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",
}){
"error": "transition from \"Unprocessed\" to \"Processed\" is not allowed through the Partner API"
}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.
Every create body accepts an externalReferences object.
Keys are merged into the existing map; a null value deletes that key.
Max 10 keys per record · keys match [A-Za-z0-9_-]{1,64} · values 1–128 chars.
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
},
})Sales source & metadata
Leads, quotes, and orders can carry marketing attribution alongside the customer record. Both fields are optional on create and PATCH; omitted keys are a no-op (backward compatible).
The portal Contact tab Source dropdown value. Case-sensitive — must match exactly. When set on create or PATCH, the linked customer’s source is synced. Explicit null on PATCH clears the field and the customer.
Arbitrary JSON key/value attribution stored alongside salesSource (site path, UTM params, campaign ids, …). Values may be strings, numbers, booleans, objects, or arrays. PATCH merges keys — per-key null deletes a key; top-level null clears all. Max 25 keys, 8192 bytes serialized. Does not sync to the customer.
Allowed sales source values
Facebook, Website, Landing Page, Website Chat, Walk In, Inbound Call, Referral, Facebook Marketplace, Trade Show, YouTube, Social Media, Blog, 3D configurator, Request Quote form
Both fields are returned on GET and list responses when present.
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]" },
"salesSource": "Website",
"sourceMetadata": { "utm_campaign": "spring-sale", "landingPath": "/inventory" }
}'await client.quotes.create({
serialNumber: "SC-2024-00123",
customer: { name: "Jane Doe", email: "[email protected]" },
salesSource: "Website",
sourceMetadata: { utm_campaign: "spring-sale" },
});_, err := client.Quotes.Create(ctx, partnerapi.QuoteCreateRequest{
SerialNumber: "SC-2024-00123",
Customer: partnerapi.QuoteCreateCustomer{Name: "Jane Doe", Email: "[email protected]"},
SalesSource: "Website",
SourceMetadata: map[string]any{"utm_campaign": "spring-sale"},
})curl -X PATCH "__BASE__/partner/v1/leads/665f...8293" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"salesSource": "Facebook",
"sourceMetadata": { "utm_campaign": "retarget", "oldKey": null }
}'await client.leads.update(leadId, {
salesSource: "Facebook",
sourceMetadata: { utm_campaign: "retarget", oldKey: null },
});src := "Facebook"
_, err := client.Leads.Update(ctx, leadID, partnerapi.LeadPatchRequest{
SalesSource: &src,
SourceMetadata: map[string]any{"utm_campaign": "retarget"},
})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.
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))ETag: "5"
{ "error": "version mismatch: the record is at version 5, not 4 — re-read it and retry with the current version" }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.
Stored response replayed with Idempotency-Replayed: true.
422 Unprocessable Entity.
409 Conflict — retry shortly.
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).
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"))Content-Type: application/json
Idempotency-Replayed: trueVersioning policy
The API version is carried in the path: /partner/v1. Within v1 the contract is additive-only:
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.
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.
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.
# 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.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-08-25
- Revenue / forecast read API — GET /partner/v1/revenue-forecast returns Revenue/Forecast KPIs and Authorized/Delivery week buckets for a selected Period. Qualification is delivery / promised dates; money is the Sales Ledger order amount so a related order matches GET /partner/v1/sales-ledger newOrderAmount. Period totals do not equal the Sales Ledger report for the same calendar window. New scope partner-api.revenue-forecast.read. See revenue / forecast.
2026-08-24
- Locations prefer portal (localized) contact fields — GET /partner/v1/locations and GET /partner/v1/locations/{id} now read name, address, phone, contact person, contact email, code, city, state, and zip from the localized fields the location edit screen writes. Create-time master mirrors on the localized row are used only when the localized value is blank. Response shape is unchanged.
2026-07-30
- Sales ledger read API — GET /partner/v1/sales-ledger returns sales-order ledger movement rows (same data as the portal Sales Ledger). Filter with saleDateFrom / saleDateTo for store / month reporting; ledgerDate stays the status/event register. New scope partner-api.sales-ledger.read. See sales ledger.
- Sales ledger cancelled flag — each movement includes sticky cancelled when a previously sold order later moves to Cancelled or Deleted. Filter with cancelled=true|false.
2026-07-29
- Quotes/orders honor Hide from Partner API locations — quotes and orders whose sales location is flagged “Hide from Partner API” are excluded from GET /partner/v1/quotes and GET /partner/v1/orders, and return 404 on get, patch, and status. Leads are unchanged.
2026-07-28
- Lot-stock typed photo galleries — GET /partner/v1/lot-stock returns frontImages, backImages, leftImages, rightImages, exteriorImages, and interiorImages (public CDN URL arrays for the matching work-order file types). Empty arrays when none are registered. Existing images and heroImages are unchanged. SDK types updated (LotStockItem).
2026-07-27
- Orders: sale date filter + sold pricing snapshot — orders expose saleDate and optional soldPricing (basePrice, upgradesAmount, deliveryFee, taxAmount, subtotal, total, plus surcharge/discount siblings). These are stamped when an order enters the company Sold-by Status (portal Company Settings) and stay independent of live pricing. List with saleDateFrom / saleDateTo (RFC 3339 or YYYY-MM-DD). PATCH /partner/v1/orders/{id} can set saleDate and soldPricing (partial; top-level null clears).
2026-07-25
- Location sales budgets — GET /partner/v1/location-budgets, GET /partner/v1/location-budgets/{id}, POST /partner/v1/location-budgets, and PUT /partner/v1/location-budgets/{id} expose the same per-location sales targets the portal manages at /v1/budget/sales (annual/monthly amounts, distribution percentages, lead targets, bonus thresholds). One budget per company/location/year; weekly amounts are derived server-side. New scopes partner-api.location-budgets.read / .write. See location budgets. Scope constants ship in all five SDKs; typed client methods follow in a later SDK release.
2026-07-20
- Inventory card discount on lot-stock and linked quotes/orders — GET /partner/v1/lot-stock returns discount and discountReason when set on a unit; price reflects the discounted building subtotal. When a quote is created from lot stock (or a work order is linked to a quote/order), a valid card discount is copied to pricing.discount and pricing.discountReason on the sales entity (same as the portal In Stock flow). SDK types updated (PartnerPricing, LotStockItem).
2026-07-19
- Site events — visitor behavioral tracking — POST /partner/v1/site-events ingests batched behavioral events from partner marketing sites (page views, CTA clicks, product/image views, filters, lead capture) into the same analytics pipeline as ShedCloud’s 3D configurator tracker; GET /partner/v1/site-events reads the company’s tracked events from both channels, newest first, cursor paginated (~90-day retention). A stable visitor_id (passed to configurator links via ?scv=) stitches a shopper’s journey across properties into a lifetime visitor profile, powering the portal’s new Customer Journey dashboard. New scopes partner-api.site-events.write / .read. Batch-only, max 25 events per call; server-side use only. See site events. SDK support in all five SDKs.
2026-07-17
- Lot stock store expiration — GET /partner/v1/lot-stock no longer hides units the moment a linked sales order reaches Processed. Units stay on the feed for a grace period (default 30 days from the Processed date) unless showWhenExpired is true. Response adds storeExpirationDays, showWhenExpired, and storeExpirationAnchorAt. Pass ignoreStoreExpiration=true to include units past that grace window (sold still indicates Processed). (npm 0.5.3, Go v0.5.3)
- Sales source on leads, quotes, and orders — optional salesSource on create and PATCH sets the portal Contact tab Source. Omitted = no-op. When provided, the linked customer is synced. Returned on GET/list. (npm 0.5.3, Go v0.5.3)
- Source metadata on leads, quotes, and orders — optional sourceMetadata object stores arbitrary JSON key/value attribution alongside salesSource. PATCH merges keys; top-level null clears all. (npm 0.5.3, Go v0.5.3)
- Company profile — GET /partner/v1/company returns the authenticated company’s profile (name, contact info, address, timezone, logo URL, company type, sell types). Scope partner-api.company.read.
- Lot stock siding filter — GET /partner/v1/lot-stock accepts siding (product id or display-name substring).
- Lot stock hero images — heroImages on each unit (public CDN URLs for uploaded work-order hero/product images). (npm 0.5.3)
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 writes — POST /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 writes — POST /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 creation — POST /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 creation — POST /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 writes — POST /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 writes — POST /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)
- Domains — GET /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 & webhooks — GET /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 concurrency — ETag 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 merge — POST /partner/v1/customers/{id}/merge.
2026-07-10
- Lot stock exterior attributes — attributes (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 creation — POST /partner/v1/leads with automatic salesperson routing.
- Quote → order conversion — POST /partner/v1/quotes/{id}/convert.
- Idempotency keys — Idempotency-Key on all create endpoints (24h replay window).
- Store hours — storeHours 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).