If your account manages multiple child organizations (also called secondary orgs) under a single OEM or holding account, you can automate the entire onboarding process instead of configuring each new org by hand. This workflow lets you create a new child org, apply a fully configured template org's settings to it, confirm the setup matches, and activate it, all through the Pipe17 API.
Use this workflow to:
- Create a new child organization under your holding org
- Clone roles, users, integrations, automation rules, and order routing from an existing template org
- Confirm a new child org matches your template before it goes live
- Reduce the manual setup work involved in bringing a new brand or client online
Before you begin
- A holding (primary) org with multi-org support enabled on your account. If you are not sure whether your account supports multiple child orgs, contact support.
- A template org: an existing, fully configured child org that represents your standard setup. Every new org is cloned from this one, so keep it accurate and up to date.
- The ability to make authenticated HTTP requests (for example, with curl or your application's HTTP client).
How it works
- Holding org: your primary OEM account. It owns one or more child orgs.
- Child org (secondary org): an org created under your holding org, typically representing one brand or client.
- Template org: a child org you configure once by hand and treat as the standard. New child orgs are cloned from it.
The onboarding sequence is:
- Request a management key from Pipe17
- Create the child org
- Clone configuration from the template org
- Verify the clone
- Complete a short list of manual follow-up tasks (credentials, physical locations)
- Activate the org
Steps
Step 1: Request your Child-Org Management Key
This automation runs on a privileged API key called the Child-Org Management Key, issued on your holding org. This key is different from a standard org API key: it is not self-service, and only Pipe17 can issue it.
The key can, across every child org your holding org owns:
- Create new child orgs
- Add and manage users
- Add and configure integrations
- Update settings, routing, and automations
To request one, submit a support request or email support@pipe17.com and include:
- Your holding org name and org key
- A note that you are automating child-org onboarding and need the cross-child management key
- Confirmation that the key should be able to create child orgs and manage their users, integrations, and settings
Store the key the same way you would store any highly privileged credential, for example in a secrets manager. Because it can modify every child org under your account, treat it with the same care as a production credential.
Throughout this article, $MANAGEMENT_KEY refers to the key Pipe17 issues you in this step.
Step 2: Authenticate your requests
All requests go to:
https://api-v3.pipe17.com/api/v3
Authenticate with your Child-Org Management Key in the X-Pipe17-Key header. This is the only header the API accepts for this purpose; Authorization and X-Master-Key are not supported.
export MANAGEMENT_KEY="<the key Pipe17 issued you>" export BASE="https://api-v3.pipe17.com/api/v3" curl -s "$BASE/organizations" \ -H "X-Pipe17-Key: $MANAGEMENT_KEY"
To scope a request to one child org, append ;orgKey:<childOrgKey> to the key value:
curl -s "$BASE/integrations" \ -H "X-Pipe17-Key: $MANAGEMENT_KEY;orgKey:59b7c3c1a7dbcc9a"
Important: Passing orgKey as a query or body parameter, or using an X-Org-Key header, does not work. The ;orgKey: suffix on the key value is the only supported method.
When cloning configuration between orgs, set two scoped keys:
export SRC_KEY="$MANAGEMENT_KEY;orgKey:<templateOrgKey>" # read from the template org export DST_KEY="$MANAGEMENT_KEY;orgKey:<newChildOrgKey>" # write to the new child org
If a request returns a 403, contact support to confirm your key's permissions rather than requesting a new key.
Step 3: Create the child org
Send POST /organizations with your management key. name, tier, email, phone, timeZone, and address are all required, and address itself requires firstName, lastName, and address1. A simple approach is to reuse the address, phone, and time zone from your holding org (read them first with GET /organizations/<holdingOrgKey>).
Include ?ownerMode=owner in the URL so the org's email receives an admin invite. Without it, ownership defaults to the calling key's user, and because the management key has no associated user, no admin account is created.
curl -s -X POST "$BASE/organizations?ownerMode=owner" \
-H "X-Pipe17-Key: $MANAGEMENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Widgets",
"email": "admin@acme.com",
"tier": "basic",
"phone": "+1-555-0100",
"timeZone": "America/Los_Angeles",
"address": { "firstName": "Ada", "lastName": "Admin",
"address1": "1 Main St", "city": "San Francisco",
"stateOrProvince": "CA", "zipCodeOrPostalCode": "94105",
"country": "US" }
}'-
tieracceptsfree,basic, orenterprise. - The street field is
address1.
The response includes the new orgKey. Save it, since every later step needs it. A new org starts in pending status and stays there until you activate it in Step 7.
{ "organization": { "orgKey": "59b7c3c1a7dbcc9a", "name": "Acme Widgets", "status": "pending" } }Set your scoped keys now (Step 2), using this orgKey as <newChildOrgKey>.
If create returns a 400 or 403, the cause is usually account-level rather than the request itself. A 400 with a message such as "MultiOrg disallowed for account" means your holding account needs multi-org enabled. Accounts also have limits on the number of secondary and production orgs. In either case, contact support to raise the limit or enable multi-org; requesting a new key will not resolve it.
Step 4: Clone configuration from your template org
Clone one component at a time, in this order:
custom roles → users → org profile → integrations → automation engine
→ order routing engine → shipping method mappingThe order matters because users reference roles by name, and automation and routing rules filter on integrations that need to exist first.
Custom roles
Roles come in two kinds: public roles, which exist in every org by default and should not be copied, and custom roles, which the org created (isPublic: false). The list response omits permissions, so fetch each role's detail before recreating it.
# List roles in the template; keep only custom roles (isPublic == false)
curl -s "$BASE/roles" -H "X-Pipe17-Key: $SRC_KEY"
# Fetch full detail for each custom role
curl -s "$BASE/roles/<roleId>" -H "X-Pipe17-Key: $SRC_KEY"
# Create it in the new child org
curl -s -X POST "$BASE/roles" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse Manager",
"description": "Can manage fulfillments and shipments",
"methods": { ...copied from the detail response... }
}'Send methods as an object mapping each entity to a permission string, for example { "orders": "crudl", "shipments": "rl" }, exactly as returned by the template's role detail. Submitting the wrong type (an array instead of an object) does not return an error, so verify the structure before creating the role, or the resulting role will not grant the expected permissions.
A 409 means the role already exists. Treat it as success.
Users
Recreate each user with their role assignments. Match each role name against the roles that now exist in the child org; if a role is missing, fall back to a safe default such as Administrator.
# List roles in the new child org so you can validate assignments
curl -s "$BASE/roles" -H "X-Pipe17-Key: $DST_KEY"
# List users in the template
curl -s "$BASE/users" -H "X-Pipe17-Key: $SRC_KEY"
# Create each user in the child org
curl -s -X POST "$BASE/users" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "picker@acme.com",
"name": "Pat Picker",
"roles": ["Warehouse Manager"]
}'A 409 means the user already exists. Treat it as success. Newly created users need to accept an invitation before they can log in (see Step 6).
Org profile
Copy the template org's address, phone, time zone, and settings flags onto the child org.
curl -s "$BASE/organizations/<templateOrgKey>" -H "X-Pipe17-Key: $SRC_KEY"
curl -s -X PUT "$BASE/organizations/<newChildOrgKey>" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": { ...from template... },
"phone": "+1-555-0100",
"timeZone": "America/Los_Angeles",
"settings": [ ...from template... ]
}'Integrations
Integrations fall into two categories:
- Engines: Pipe17's built-in processing engines (Order Routing, Inventory, Exception, Order Aggregator, Payment). The app automatically installs these, along with default filters and automations, as soon as you create the child org in Step 3. Because that happens in the background, wait for it to finish before comparing integration counts in Step 5. This background process skips any connector that already exists, so there is no risk of duplicates even if your clone script reaches the plugin steps before it finishes. Since engines never carry credentials and already exist in the child org, you only need to copy their settings, not create them.
- Plugins: external connectors such as Shopify, Amazon, or NetSuite. These are not installed automatically and need to be cloned.
To identify which integrations in your template are engines versus plugins, call GET /integrations in the template org rather than relying on a fixed list, since connector IDs can differ between environments.
Important: A full read of an integration returns live connection credentials. Credential masking is available but off by default, so assume credentials are visible in the response unless you have confirmed masking is enabled on your account. When cloning, exclude the connection object and any field marked secret: true from what you copy forward. You will connect each integration in the new org using its own credential flow in Step 6.
The reliable pattern is create a minimal shell, read back the defaults, merge in the template's values, then update:
# List integrations in the template
curl -s "$BASE/integrations" -H "X-Pipe17-Key: $SRC_KEY"
# Fetch full detail for one integration
curl -s "$BASE/integrations/<templateIntegrationId>" -H "X-Pipe17-Key: $SRC_KEY"
# Create a minimal shell in the child org (connector ID and name only)
curl -s -X POST "$BASE/integrations" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{ "connectorId": "f1210e73c0eba8d6", "integrationName": "Shopify Integration" }'
# Read back the child org's default settings
curl -s "$BASE/integrations/<newIntegrationId>" -H "X-Pipe17-Key: $DST_KEY"
# Merge: start from the child's default fields and overlay the template's values,
# matching on "path". This preserves required fields the template never set.
# Update the child integration with the merged settings
curl -s -X PUT "$BASE/integrations/<newIntegrationId>" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{ "settings": { "fields": [ ...merged... ] }, "entities": [ ...from template... ] }'For engines, which already exist, skip the create step and go straight to read-back, merge, and update. If a connector already exists in the child org, reuse its integrationId instead of creating a new one.
Two settings details worth knowing:
- Fields ending in
.since(for example,orders.in.since) are sync cursors. Give each one a plain ISO-8601 date string so the new org starts syncing from onboarding time:{ "path": "orders.in.since", "value": "2026-07-09T21:00:36.697Z", "type": "date" }. Send the value as a plain string, not wrapped in a{ "__type": "Date", "iso": "..." }object. On these fields, the wrapped format returns a "cannot be converted to a date" error. - Because you start from the child's default fields and overlay the template's values, required fields the template never set keep their defaults instead of being left blank.
Custom field mappings: Plugin integrations reference field mappings by mappingUuid. Public (global) mappings are shared across all orgs and should be left as-is. A custom mapping (isPublic: false and owned by the template org) needs to be cloned and repointed:
# Look up the mapping; if isPublic is true or it belongs to a different org, skip it
curl -s "$BASE/mappings?uuid=<mappingUuid>" -H "X-Pipe17-Key: $SRC_KEY"
# Fetch full detail for a custom mapping
curl -s "$BASE/mappings/<mappingId>" -H "X-Pipe17-Key: $SRC_KEY"
# Recreate it in the child org
curl -s -X POST "$BASE/mappings" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Custom order mapping",
"isPublic": false,
"mappings": [ ...from the detail response... ]
}'Send only description, isPublic, and mappings. Including connectorId returns a 400 error, since it is not part of the mapping schema. Use the new uuid from the response to update the entity's mappingUuid.
Automation engine
Automation configuration has two layers: triggers (matched by type, such as NEW_ORDER or ORDER_UPDATED) and the rules attached to them. Match triggers by type and use the child org's own trigger ID, since IDs are not portable across orgs.
# Read triggers in both orgs and map trigger type to trigger ID for each
curl -s "$BASE/automations" -H "X-Pipe17-Key: $SRC_KEY"
curl -s "$BASE/automations" -H "X-Pipe17-Key: $DST_KEY"
# If the template has rules for a trigger type the child org lacks, create it
curl -s -X POST "$BASE/automations" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{ "isEnabled": true, "trigger": "ORDER_UPDATED", "isPublic": false }'
# List rules in the template and fetch detail for each
curl -s "$BASE/automation_rules" -H "X-Pipe17-Key: $SRC_KEY"
curl -s "$BASE/automation_rules/<automationRuleId>" -H "X-Pipe17-Key: $SRC_KEY"
# Create each rule in the child org, using the child org's trigger ID
curl -s -X POST "$BASE/automation_rules" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Hold high-value orders",
"description": "...",
"automationId": "<child-org trigger ID for this type>",
"isEnabled": true,
"actions": [ ...from template rule... ],
"filter": { ...from template rule... }
}'Default rules (isDefault: true) exist in every org already and cannot be deleted. If a template's default rule differs from the child's, update the child's rule instead of trying to create a new one:
curl -s -X PUT "$BASE/automation_rules/<childDefaultRuleId>" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "...", "isEnabled": true, "actions": [ ... ], "filter": { ... } }'Order routing engine
Order routing has two parts:
- The routing engine settings, which are an integration and follow the same clone pattern described above.
- The routing rules, which reference filters and location groups that need to be matched or recreated per org.
For entity filters: pre-built filters exist in every org and can be matched by originId. Custom filters with no match in the child org need to be recreated, and the rule's filterId updated to point at the new one. The create call requires name, query, and entity; omitting entity fails validation.
For location groups: match by groupName. If a group is missing, create it with a placeholder location, since the API requires at least one. Replace the placeholder with real locations in Step 6.
# Entity filters
curl -s "$BASE/entity_filters" -H "X-Pipe17-Key: $SRC_KEY"
curl -s "$BASE/entity_filters" -H "X-Pipe17-Key: $DST_KEY"
curl -s "$BASE/entity_filters/<filterId>" -H "X-Pipe17-Key: $SRC_KEY"
curl -s -X POST "$BASE/entity_filters" \
-H "X-Pipe17-Key: $DST_KEY" -H "Content-Type: application/json" \
-d '{ "name": "...", "entity": "<from template filter>", "originId": "<template filterId>", "query": { ... } }'
# Location groups
curl -s "$BASE/location_groups" -H "X-Pipe17-Key: $SRC_KEY"
curl -s "$BASE/location_groups" -H "X-Pipe17-Key: $DST_KEY"
curl -s -X POST "$BASE/location_groups" \
-H "X-Pipe17-Key: $DST_KEY" -H "Content-Type: application/json" \
-d '{ "groupName": "West Coast", "locationIds": ["__placeholder__"] }'
# Routing rules
curl -s "$BASE/order_routings" -H "X-Pipe17-Key: $SRC_KEY"
curl -s "$BASE/order_routings/<routingId>" -H "X-Pipe17-Key: $SRC_KEY"
# Update every filterId and groupId in the rules to the child org's IDs, then:
curl -s -X POST "$BASE/order_routings" \
-H "X-Pipe17-Key: $DST_KEY" -H "Content-Type: application/json" \
-d '{ "rules": [] }'
curl -s -X PUT "$BASE/order_routings/<childRoutingId>" \
-H "X-Pipe17-Key: $DST_KEY" -H "Content-Type: application/json" \
-d '{ "rules": [ ...rewritten... ] }'Shipping method mapping
Copy each shipping method from the template, dropping system-assigned fields such as IDs and timestamps.
-
sourceTypeis required, so make sure it survives the cleanup step. -
sourceIntegrationIdrefers to an integration in a specific org. Update it to point at the child org's cloned integration, or omit it, since the API accepts an invalid reference without validating it at create time.
curl -s "$BASE/shipping_methods" -H "X-Pipe17-Key: $SRC_KEY"
curl -s -X POST "$BASE/shipping_methods" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Ground", "sourceType": "<from template>",
"sourceIntegrationId": "<child integration id, or omit>",
...other fields from template... }'A 409 means the method already exists. Treat it as success.
Step 5: Verify the clone
Confirm the child org matches the template:
curl -s "$BASE/integrations" -H "X-Pipe17-Key: $DST_KEY" curl -s "$BASE/order_routings" -H "X-Pipe17-Key: $DST_KEY" curl -s "$BASE/automation_rules" -H "X-Pipe17-Key: $DST_KEY" curl -s "$BASE/roles" -H "X-Pipe17-Key: $DST_KEY" curl -s "$BASE/users" -H "X-Pipe17-Key: $DST_KEY"
Spot-check any custom mapping you cloned. In the child org it should show isPublic: false and belong to the new child org, confirming it is a genuine clone rather than a reference back to the template:
curl -s "$BASE/mappings?uuid=<newMappingUuid>" -H "X-Pipe17-Key: $DST_KEY"
Step 6: Complete manual follow-up tasks
A few tasks are intentionally left for a person to complete, since they are specific to each org and involve sensitive credentials:
- Integration credentials: Cloned integrations have settings but no connection credentials. Connect each one (Shopify, Amazon, NetSuite, and so on) through its usual credential flow.
- Physical locations: Location groups were created with a placeholder location. Add the org's real locations, then remove the placeholder.
- Inventory and Exception engine tuning: Some engine configuration is only available in the app, not the API.
- User invitations: Cloned users need to accept their invitation before they can log in.
Step 7: Activate the organization
A new org stays in pending status until you activate it. Once cloning, credentials, and locations are in place, update the status:
curl -s -X PUT "$BASE/organizations/<newChildOrgKey>" \
-H "X-Pipe17-Key: $DST_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "active" }'Do this last. An active org begins processing orders immediately. If you would rather have someone review the setup first, leave the org in pending status and hand it off for a final check before activating.
API quick reference
All calls use the X-Pipe17-Key header. "Management key" refers to the raw Child-Org Management Key; "scoped key" refers to the key with ;orgKey:<childOrgKey> appended.
| Action | Method and path | Key |
|---|---|---|
| List or read orgs | GET /organizations[/<orgKey>] |
Management |
| Create child org | POST /organizations |
Management |
| Update org profile | PUT /organizations/<orgKey> |
Scoped |
| Roles |
GET/POST /roles, GET /roles/<id>
|
Scoped |
| Users |
GET/POST /users
|
Scoped |
| Integrations |
GET/POST /integrations, GET/PUT /integrations/<id>
|
Scoped |
| Field mappings |
GET /mappings[?uuid=], GET /mappings/<id>, POST /mappings
|
Scoped |
| Automations (triggers) |
GET/POST /automations
|
Scoped |
| Automation rules |
GET/POST/PUT /automation_rules[/<id>]
|
Scoped |
| Order routing |
GET/POST /order_routings, GET/PUT /order_routings/<id>
|
Scoped |
| Entity filters |
GET/POST /entity_filters, GET /entity_filters/<id>
|
Scoped |
| Location groups |
GET/POST /location_groups
|
Scoped |
| Shipping methods |
GET/POST /shipping_methods
|
Scoped |
Pagination
List endpoints used throughout this workflow, such as /roles, /users, and /integrations, return up to 100 records per call by default. Use skip and count to page through larger result sets, for example ?count=100&skip=100 for the second page, and stop once the returned array is shorter than count. There is no limit parameter. A request with ?limit=100 returns a 400 error rather than being ignored, since unrecognized query parameters are rejected.
Best practices
- Keep your template org current. Every new child org inherits its configuration, so outdated settings in the template carry forward to every org you create.
- Test the full sequence against a non-production account before running it against real customers.
- Pace your requests using the rate-limit headers returned on every response (
x-rate-limit,x-burst-limit,x-quota-limit), and back off with increasing delays if you receive a 429 response, since the API does not return aRetry-Afterheader. - Leave a new org in
pendingstatus if you want a person to review the configuration before it goes live.
Troubleshooting
Creating a child org returns a 400 or 403 error
This is usually an account-level limit rather than a problem with your request. Contact support to confirm multi-org is enabled and that you have not reached your account's secondary-org limit.A role was created but does not grant the expected permissions
Check thatmethodswas sent as an object mapping entities to permission strings, not an array. An incorrect format is accepted without an error but produces a non-functional role.A default automation rule cannot be deleted
Default rules exist in every org by design and cannot be removed. Update the rule'sactionsandfilterinstead of trying to recreate it.An integration create or update request rejects a required field
The child org's connector schema may expect a different set of fields than the template. Read back the child's default settings first and merge the template's values into them, rather than sending the template's settings directly.Creating a field mapping returns a 400 "Invalid parameter" error for connectorId
The mapping creation endpoint only acceptsdescription,isPublic, andmappings. RemoveconnectorIdfrom the request.A list request returns a 400 "Invalid parameter" error
There is nolimitparameter. Useskipandcountinstead; see Pagination above.
Limitations
- Inventory and Exception engine tuning is only available in the app, not through the API.
- New orgs do not activate automatically. You must explicitly update the status.
- Cloned users must accept an invitation before they can log in.
- Location groups require at least one location at creation, so a placeholder is needed until real locations are added.
Need Help?
If you need additional assistance:
Use Ask Pippen, our AI agent, located at the top of the app page.
Submit a support request with as much relevant detail as possible. Learn how to submit a request.
For urgent issues, email us directly at support@pipe17.com.
We're here to help you succeed with your operations.
Comments
0 comments