Upsert variants and providers
Creates or updates service variants and providers (professionals) in a single API call.
/api/integration/unified/upsertRequest
curl -X POST "https://dev.integration.cl.sodtrack-shared.sodtrack.com/api/integration/unified/upsert" \
-H "Authorization: Bearer $SODTRACK_TOKEN"Base URL: https://dev.integration.cl.sodtrack-shared.sodtrack.com · other environments
Responses
{
"success": true,
"message": "All operations completed successfully",
"variantsProcessed": 1,
"providersProcessed": 1,
"variantResults": [
{
"variantId": 123,
"sku": "SV-001",
"success": true
}
],
"providerResults": [
{
"providerId": 456,
"reference": "PROV-001",
"success": true
}
]
}Error responses for this endpoint follow the shared error reference.
Purpose
Creates or updates service variants and providers (professionals) in a single API call.
This endpoint manages the complete lifecycle of service variants identified by SKU, providers identified by reference, and their associated configurations including provider-variant associations, availability schedules, and working areas.
The endpoint processes execution variants only (not budgeted variants).
This endpoint is specifically designed for external integration systems to synchronize professional data and service configurations. It supports creating or updating professionals that are either natural persons or networks, but must not be used for technicians that belong to a company.
Field Definitions
🗂️ Request Body
| Field | Type | Required | Description |
|---|---|---|---|
providers | array[object] | ❌ | Array of provider objects to create or update. If omitted, only variants will be processed. |
variants | array[object] | ❌ | Array of service variant objects to create or update. If omitted, only providers will be processed. |
👷 Provider
| Field | Type | Required | Description |
|---|---|---|---|
reference | string | ✅ | Unique reference identifier for the provider. Used to identify existing providers for updates. |
status | string | ❌ | Status for the provider. If not included, default status for new provider is accepted. If included, must be accepted or rejected. |
basicInformation | object | ✅ | Basic user information for the provider. See Provider Basic Information below. |
workOnHolidays | boolean | ❌ | Whether the provider works on holidays. Defaults to false if not provided. |
isCompany | boolean | ❌ | Whether the provider is a company. Defaults to false if not provided. |
isMainProviderEligible | boolean | ❌ | Whether the provider is eligible to be assigned as the main provider. Defaults to true if not provided. |
isLeadAssignmentEligible | boolean | ❌ | Whether the provider is eligible for lead assignment. Defaults to true if not provided. |
service_variants | array[object] | ❌ | Array of service variants associated with this provider. Variants can be referenced by ID or SKU. |
availability | array[object] | ❌ | Array of availability schedules defining when the provider is available. |
areas | array[object] | ❌ | Array of working areas where the provider operates. Areas can be referenced by ID or reference. |
🪪 Provider Basic Information
| Field | Type | Required | Description |
|---|---|---|---|
basicInformation.name | string | ✅ | Provider's first name. |
basicInformation.lastName | string | ✅ | Provider's last name. |
basicInformation.phoneNumber | string | ✅ | Provider's phone number. |
basicInformation.email | string | ✅ | Provider's email address. Must be unique in the system. |
basicInformation.identificationNumber | string | ✅ | Provider's identification number (e.g., national ID, passport). |
basicInformation.identificationNumberCountry | string | ✅ | Country code for the identification number. |
basicInformation.reference | string | ❌ | Optional reference identifier (separate from provider reference). |
📅 Provider Availability
| Field | Type | Required | Description |
|---|---|---|---|
availability[].day_of_week | number | ✅ | Day of the week (0 = Sunday, 1 = Monday, 2 = Tuesday, 3 = Wednesday, 4 = Thursday, 5 = Friday, 6 = Saturday). |
availability[].start_time | string | ✅ | Start time in HH:mm format (e.g., "09:00"). |
availability[].end_time | string | ✅ | End time in HH:mm format (e.g., "17:00"). |
Availability is fully replaced on update. All previous availability records are deleted and replaced by the incoming set.
🗺️ Provider Areas
| Field | Type | Required | Description |
|---|---|---|---|
areas[].type | string | ✅ | Type of area identifier. Must be one of: "areaId" or "areaReference". |
areas[].value | string | ✅ | The area ID (if type is "areaId") or area reference (if type is "areaReference"). |
areas[].variantCoverage | array[object] | ❌ | Optional list of service variants covered by this provider specifically in this area (differentiated coverage). When omitted or empty, the provider covers all their assigned variants in this area (total coverage). |
areas[].variantCoverage[].type | string | ✅ when variantCoverage sent | Type of variant identifier. Must be one of: "variantId" or "variantSku". |
areas[].variantCoverage[].value | string | ✅ when variantCoverage sent | The variant ID (if type is "variantId") or SKU (if type is "variantSku"). |
When
variantCoverageis provided for an area, only the listed variants will be restricted to that specific working area. Variants not listed in anyvariantCoverageentry retain total coverage (unrestricted).
🔧 Variant
| Field | Type | Required | Description |
|---|---|---|---|
sku | string | ✅ | Stock Keeping Unit identifier. Must be unique across all variants. Used to identify existing variants for updates. |
name | string | ✅ | Internal name of the service variant. |
isInStore | boolean | ✅ | Whether this variant is available for in-store services. |
isExecution | boolean | ✅ | Whether this is an execution variant. Only execution variants (true) are processed by this endpoint. Budget variants (false) are skipped. |
parentService | object | ✅ | Parent service information. If the service or category doesn't exist, it will be created. See Parent Service below. |
operationAreas | object | ❌ | Operation area configurations with detailed (specific areas) and/or all (default parameters for remaining areas). |
🏷️ Parent Service
| Field | Type | Required | Description |
|---|---|---|---|
parentService.name | string | ✅ | Name of the service. Matching is done using normalized string comparison (case-insensitive, accent-insensitive). |
parentService.description | string | ❌ | Description of the service. |
parentService.category.name | string | ✅ | Name of the category. Matching is done using normalized string comparison (case-insensitive, accent-insensitive). |
parentService.category.description | string | ❌ | Description of the category. |
🗺️ Operation Areas
| Field | Type | Required | Description |
|---|---|---|---|
operationAreas.all | object | ❌ | Default parameters applied to all active areas not explicitly covered by detailed. Allows setting default pricing and duration for all remaining areas. |
operationAreas.detailed | array[object] | ❌ | Array of specific operation area configurations. Each entry defines pricing, duration, and area coverage for specific areas. detailed entries take precedence over all. |
operationAreas.detailed[].type | string | ✅ when detailed sent | Type of area identifier. Must be one of: "areaId" or "areaReference". |
operationAreas.detailed[].value | array[string] | ✅ when detailed sent | Array of area identifiers. Areas are automatically grouped by currency for operation variant creation. |
operationAreas.detailed[].duration | string | ❌ | Duration of the operation in minutes (as a string, e.g., "60"). |
operationAreas.detailed[].bookingAnticipationHours | number | ❌ | Number of hours in advance a booking must be made for this operation. |
operationAreas.detailed[].price | number | ❌ | Price for this operation variant. |
operationAreas.detailed[].cost | number | ❌ | Cost for this operation variant. |
operationAreas.detailed[].requireEvidence | boolean | ❌ | Whether evidence is required for this operation. |
operationAreas.detailed[].requireSignature | boolean | ❌ | Whether a signature is required for this operation. |
operationAreas.all.duration | string | ❌ | Default duration in minutes (as a string, e.g., "60"). Applied to all remaining areas not covered by detailed. |
operationAreas.all.bookingAnticipationHours | number | ❌ | Default number of hours in advance a booking must be made. Applied to all remaining areas not covered by detailed. |
operationAreas.all.price | number | ❌ | Default price. Applied to all remaining areas not covered by detailed. |
operationAreas.all.cost | number | ❌ | Default cost. Applied to all remaining areas not covered by detailed. |
operationAreas.all.requireEvidence | boolean | ❌ | Default requirement for evidence. Applied to all remaining areas not covered by detailed. |
operationAreas.all.requireSignature | boolean | ❌ | Default requirement for signature. Applied to all remaining areas not covered by detailed. |
Example Request
{
"variants": [
{
"sku": "SV-001",
"name": "Standard Cleaning Service",
"isInStore": false,
"isExecution": true,
"parentService": {
"name": "Cleaning Services",
"description": "Professional cleaning services",
"category": {
"name": "Home Services",
"description": "Services for home maintenance"
}
},
"operationAreas": {
"detailed": [
{
"type": "areaReference",
"value": ["AREA-001", "AREA-002"],
"duration": "120",
"price": 150.0,
"cost": 100.0,
"bookingAnticipationHours": 24
}
],
"all": {
"duration": "60",
"price": 100.0,
"cost": 50.0,
"bookingAnticipationHours": 12
}
}
}
],
"providers": [
{
"reference": "PROV-001",
"basicInformation": {
"name": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phoneNumber": "+1234567890",
"identificationNumber": "12345678",
"identificationNumberCountry": "US"
},
"isCompany": false,
"workOnHolidays": false,
"isMainProviderEligible": true,
"isLeadAssignmentEligible": true,
"service_variants": [
{
"type": "variantSku",
"value": "SV-001"
},
{
"type": "variantSku",
"value": "SV-002"
}
],
"availability": [
{
"day_of_week": 1,
"start_time": "09:00",
"end_time": "17:00"
},
{
"day_of_week": 2,
"start_time": "09:00",
"end_time": "17:00"
}
],
"areas": [
{
"type": "areaReference",
"value": "AREA-001"
},
{
"type": "areaReference",
"value": "AREA-002",
"variantCoverage": [
{
"type": "variantSku",
"value": "SV-001"
}
]
}
]
}
]
}
In this example, the provider covers AREA-001 with total coverage (all their variants) and AREA-002 with differentiated coverage restricted to SV-001 only.
Response Example
{
"success": true,
"message": "All operations completed successfully",
"variantsProcessed": 1,
"providersProcessed": 1,
"variantResults": [
{
"variantId": 123,
"sku": "SV-001",
"success": true
}
],
"providerResults": [
{
"providerId": 456,
"reference": "PROV-001",
"success": true
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates whether all operations completed successfully. |
message | string | Human-readable message describing the operation result. |
variantsProcessed | number | Number of variants successfully processed. |
providersProcessed | number | Number of providers successfully processed. |
errors | array[string] | Array of error messages if any operations failed. Each message includes the identifier (SKU or reference) and the error description. |
variantResults | array[object] | Detailed processing results for each variant. |
variantResults[].variantId | number | The ID of the variant that was processed. |
variantResults[].sku | string | The SKU identifier of the variant. |
variantResults[].success | boolean | Whether this specific variant was processed successfully. |
variantResults[].error | string | Error message if the variant processing failed. Only present when success is false. |
providerResults | array[object] | Detailed processing results for each provider. |
providerResults[].providerId | number | The ID of the provider that was processed. |
providerResults[].reference | string | The reference identifier of the provider. |
providerResults[].success | boolean | Whether this specific provider was processed successfully. |
providerResults[].error | string | Error message if the provider processing failed. Only present when success is false. |
Error Response Example
{
"success": false,
"message": "Completed with 2 error(s)",
"variantsProcessed": 0,
"providersProcessed": 1,
"errors": [
"Variant SV-001: Multiple variants found for the same SKU",
"Provider PROV-002: Provider cannot be part of a network"
],
"variantResults": [
{
"variantId": 0,
"sku": "SV-001",
"success": false,
"error": "Multiple variants found for the same SKU"
}
],
"providerResults": [
{
"providerId": 456,
"reference": "PROV-001",
"success": true
},
{
"providerId": 0,
"reference": "PROV-002",
"success": false,
"error": "Provider cannot be part of a network"
}
]
}
Error Responses
| HTTP Status | Description |
|---|---|
400 Bad Request | One or more required fields are missing or invalid. |
401 Unauthorized | Authentication token missing or expired. |
403 Forbidden | The API key or origin is not authorized. |
404 Not Found | A referenced variant, provider, or area could not be found. |
500 Internal Server Error | Unexpected server error. Contact Sodtrack support. |
Business Rules & Constraints
Provider Type Restrictions
-
This endpoint only supports creating or updating professionals that are either:
-
Natural persons (individual providers with
isCompany: falseand no parent relationship) -
Networks (providers with
isCompany: truethat act as parent organizations)
-
-
It MUST NOT be used to create or update technicians that belong to a company (providers with a
parentrelationship). Attempting to update a provider that has an existing parent relationship will result in an error.
Variant SKU Uniqueness
-
Each service variant SKU sent in the request MUST be unique in the system.
-
If more than one variant is found for the same SKU, the operation for that SKU must fail and be stored as an error.
-
No partial processing occurs for that SKU — the entire variant upsert operation for that SKU is rejected.
Service and Category Changes
-
If a SKU in the variants payload matches an existing variant, but the service or category differs from the existing one:
-
The variant and all its operation variants will be moved from the existing service to the new service provided in the request.
-
If the category also differs, the variant will be moved under the new category.
-
This operation preserves all existing operation variants and their configurations — only the service and category associations are updated.
-
Operation Variants Preservation
-
Operation variants are NEVER deleted by the algorithm. The system preserves all existing operation variants and only updates or reassigns them based on the incoming payload.
-
Operation variants NEVER end without being associated to an operation area. The re-shuffle logic ensures all operation variants maintain at least one area association.
Execution Variants Only
- Only variants with
isExecution: trueare processed. Variants withisExecution: falseare skipped with a warning log entry and do not count toward thevariantsProcessedtotal.
Differentiated Area Coverage (variantCoverage)
variantCoverage configures a restriction on the variant, not on the area. All areas in provider.areas are always associated to the provider as working areas, regardless of whether they carry a variantCoverage or not.
How it works:
-
Variants listed in
variantCoveragefor an area are restricted to operate only in that area. Their provider-variant association is linked specifically to that working area. If the same variant appears invariantCoverageacross multiple areas, it is restricted to all of those areas combined. -
Variants with no
variantCoverageentry anywhere have total coverage — they operate across all working areas of the provider. -
A variant listed in
variantCoveragebut absent fromservice_variantsis automatically added as a new provider-variant association with its area restriction.
Important: if the same variant appears in both
service_variantsand in avariantCoverageentry, the restriction takes effect — the variant will be limited to that area and will not have total coverage.
Delete behavior:
-
Deletion of provider-variant associations only runs when
service_variantsis explicitly provided. -
Only associations absent from both
service_variantsand allvariantCoverageentries across all areas are removed.
Update Strategy
When a provider is sent and it already exists (identified by reference), the update follows this pattern:
workingAreas (areas field)
-
Keep: Working areas that match areas in the incoming
areasarray are preserved. -
Remove: Working areas that exist but are not in the incoming
areasarray are deleted. -
Add: New working areas are created for areas in the incoming
areasarray that don't have an existing working area association.
availabilities (availability field)
- Replace All: All existing availabilities are deleted and replaced by the new set provided in the
availabilityarray.
professionalServiceVariants (service_variants field)
-
If
service_variantsis omitted: Existing provider-variant associations are left untouched. OnlyvariantCoverageentries (fromareas) may implicitly add new associations or update working-area restrictions on existing ones. -
If
service_variantsis provided: The existing associations are fully reconciled against the incoming list:-
Keep: Associations that match variants in the incoming
service_variantsarray (orvariantCoverageentries) are preserved. -
Remove: Associations absent from both
service_variantsand allvariantCoverageentries are deleted. -
Add: New associations are created for variants in
service_variantsorvariantCoverageentries that don't have an existing association.
-
Provider basic information fields (
isMainProviderEligible,isLeadAssignmentEligible,workOnHolidays) are updated directly from the payload values. TheisCompanyfield is only set during creation and is not updated for existing providers.
Operation Variants Re-shuffle Logic
When an existing service variant is sent with operation area configurations (via the operationAreas object), the system performs intelligent re-shuffling of operation variants rather than deleting and recreating them. The algorithm supports two configuration modes: detailed (specific area configurations) and all (default parameters for remaining areas).
Processing Flow
The system processes operation areas in two phases:
Phase 1: Processing detailed Entries
-
Area Resolution and Currency Grouping
-
Each area value in each
detailedentry is resolved to an Area entity with its associated currency. -
Areas are automatically grouped by currency — one VariantOperation is created or updated per currency group.
-
The system tracks which areas have been processed in this phase.
-
-
Conflict Detection
-
For each currency group, the system identifies existing operation variants that have overlapping areas (same currency, shared area IDs).
-
If no conflicts exist, a new operation variant is created with the specified areas and parameters.
-
-
Conflict Resolution Strategy
-
Operations with Remaining Areas: If an existing operation variant has both overlapping areas (to be reassigned) and non-overlapping areas (to remain), the system:
-
Removes the overlapping areas from the existing operation variant
-
Updates the operation variant to keep only the remaining areas
-
Preserves the existing operation variant's parameters (duration, cost, price, etc.)
-
The freed overlapping areas are added to the working set for reassignment
-
-
-
Operations That Would Become Empty
-
If an existing operation variant would become empty (all its areas are being reassigned), the system:
-
Sorts these operations by area count (fewest first) for deterministic processing
-
Updates all but the last operation with the new parameters and keeps their existing areas
-
Removes these areas from the working set
-
Updates the last operation with the new parameters and assigns all remaining areas from the working set to it
-
-
-
New Operation Creation
- If any areas remain in the working set after conflict resolution, a new operation variant is created with those areas and the specified parameters.
Phase 2: Processing all Parameters (if provided)
-
Remaining Areas Identification
-
The system retrieves all active areas in the system.
-
It filters out areas already processed in Phase 1 (from
detailedentries).
-
-
Synthetic DTO Creation
- For the remaining areas, the system creates synthetic area configurations using the parameters from the
allconfiguration.
- For the remaining areas, the system creates synthetic area configurations using the parameters from the
-
Processing Remaining Areas
-
The synthetic configurations are processed using the same conflict resolution logic as Phase 1.
-
This ensures all remaining areas receive the default parameters specified in
all.
-
Key Principles
-
No Deletion: Operation variants are never deleted. They are either updated with new areas/parameters or preserved as-is.
-
Area Preservation: All areas specified in the request (either explicitly in
detailedor implicitly viaall) are assigned to operation variants — no areas are lost. -
Parameter Updates: When an operation variant is updated, its parameters (duration, cost, price, bookingAnticipationHours) are updated to match the incoming values.
-
Currency Isolation: Operation variants are always grouped by currency. Areas with different currencies are never mixed in the same operation variant.
-
Priority:
detailedentries take precedence overall. Areas specified indetailedare processed first, andallparameters are only applied to areas not covered bydetailed.
Usage Patterns
Pattern 1: Detailed Configuration Only
Use detailed when you need specific configurations for specific areas:
{
"operationAreas": {
"detailed": [
{
"type": "areaReference",
"value": ["AREA-001", "AREA-002"],
"price": 150.0,
"duration": "120"
}
]
}
}
Pattern 2: All Areas with Default Parameters
Use all when you want to apply the same parameters to all active areas:
{
"operationAreas": {
"all": {
"price": 100.0,
"duration": "60",
"cost": 50.0
}
}
}
Pattern 3: Mixed Configuration (Recommended)
Use both detailed and all when you need specific configurations for some areas and default parameters for the rest:
{
"operationAreas": {
"detailed": [
{
"type": "areaReference",
"value": ["AREA-001"],
"price": 200.0,
"duration": "180"
}
],
"all": {
"price": 100.0,
"duration": "60"
}
}
}
In this example, AREA-001 gets the specific configuration (price: 200.0, duration: 180), while all other active areas get the default parameters (price: 100.0, duration: 60).
Example Scenario
Existing State:
-
Operation Variant A: Areas [1, 2, 3], Currency USD, Price $100
-
Operation Variant B: Areas [4, 5], Currency USD, Price $80
-
System has 10 total active areas: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Incoming Request:
{
"operationAreas": {
"detailed": [
{
"type": "areaId",
"value": ["2", "3", "6"],
"price": 120.0,
"duration": "90"
}
],
"all": {
"price": 80.0,
"duration": "60"
}
}
}
Result:
-
Operation Variant A: Updated to Areas [1], Currency USD, Price $100 (areas 2, 3 removed; price preserved since not in
detailed) -
Operation Variant B: Updated to Areas [4, 5], Currency USD, Price $80 (parameters updated from
all) -
New Operation Variant C: Areas [2, 3, 6], Currency USD, Price $120, Duration 90 (created from
detailed) -
New Operation Variant D: Areas [7, 8, 9, 10], Currency USD, Price $80, Duration 60 (created from
allfor remaining areas)