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.customers.read",
"partner-api.customers.write",
"partner-api.products.read",
"partner-api.products.write",
"partner-api.users.read",
"partner-api.users.write",
"partner-api.contracts.read",
"partner-api.payments.read",
"partner-api.payments.write",
"partner-api.documents.read",
"partner-api.events.read",
"partner-api.configurator-sessions.write",
"partner-api.domains.read"
]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 }
}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).
Case-insensitive substring match on serial number or title.
Sort by serialNumber, title, price, or createdAt.
Sort direction: asc or desc.
Only sellable units are returned — units with a processed linked sales order are excluded and results are collapsed to one row per work order.
Response fields
Inventory items (see example).
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.
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": 8995.00,
"locationId": "66c00443c2d8aa83c5757dcf",
"locationName": "Dallas Lot",
"locationSlug": "dallas-lot",
"images": ["https://cdn.shedcloud.com/.../front.jpg"],
"attributes": {
"siding": "LP Smart Side Siding & Trim",
"sidingColor": "Ivory",
"trimColor": "Polar White",
"roofMaterial": "Metal Roof",
"roofColor": "Barn Red Metal"
},
"sold": false
}
],
"page": 1,
"limit": 50,
"total": 137
}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.
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" },
"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.
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" }
}'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), serialNumber, workOrderId, and conversion linkage (converted, convertedOrderId, convertedOrderNumber).
Quotes also carry an optional validUntil date (settable via PATCH or the portal). When it passes while the quote is still Open/Active a quote.expired event is emitted — the status is not changed automatically. A quote.sent event fires when the quote is shared/emailed to the customer; there is no quote viewed tracking. RTO quotes/orders include an rto block (term, monthly payment, rent, deposits, damage waiver, balance) — rto.providerName resolves on get-by-id only. There is no financing application lifecycle: the RTO provider handles approve/decline externally.
Line-item writes (scope partner-api.quotes.write): POST .../line-items adds one upgrade line (idempotent on lineKey), DELETE .../line-items/{lineId} removes one — the base product line is protected. Same semantics as on orders.
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" },
"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), 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.
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" },
"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.
Order items add pricing (including changeOrderFee when a change-order fee was applied — change orders are a fee plus payment-ledger entries, not a separate order entity), deposits, serialNumber, workOrderId, source-quote linkage (sourceQuoteId, sourceQuoteNumber), and delivery dates (expectedDeliveryDate, deliveredAt).
Create
POST /partner/v1/orders (scope partner-api.orders.write) creates a full order directly, no quote needed: a customer (existing customerId or an inline customer block matched by email / created), locationId, the base productId (+ optional sizeId), upgrades lines, pricing header (basePrice / subtotal / total / paymentType), deliveryAddress, and an optional configuration payload — pass the portal-shaped selectedCategories array verbatim, or flat siding / sidingColor / trimColor / roofMaterial / roofColor attributes. Runs the portal’s own pipeline (order number, detail rows, configurator link, status log, ledger, audit, CRM). New orders start in Unsubmitted. Supports Idempotency-Key.
Line-item writes
POST .../line-items adds one upgrade line ({ productId, quantity, price, lineKey }) — idempotent on lineKey: re-sending the same key returns the existing line (200, "created": false). DELETE .../line-items/{lineId} removes one line by its key; the base product line is protected (409). The same pair exists on quotes. Header pricing is not recalculated — PATCH the entity’s pricing fields after changing lines.
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" },
"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.
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 }
}
}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
}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 asyncConfigurator 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.
customerName, customerEmail, customerPhone, salespersonName, salespersonEmail, salesLocation, deliveryAddress, deliveryCity, deliveryState, deliveryZipCode. Quotes additionally accept validUntil (RFC 3339 or YYYY-MM-DD; null clears it).
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
},
})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-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).