Upsert variants and providers

Creates or updates service variants and providers (professionals) in a single API call.

POST/api/integration/unified/upsert

Request

cURL
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

Upsert variants and providers
{
  "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

FieldTypeRequiredDescription
providersarray[object]❌Array of provider objects to create or update. If omitted, only variants will be processed.
variantsarray[object]❌Array of service variant objects to create or update. If omitted, only providers will be processed.

πŸ‘· Provider

FieldTypeRequiredDescription
referencestringβœ…Unique reference identifier for the provider. Used to identify existing providers for updates.
statusstring❌Status for the provider. If not included, default status for new provider is accepted. If included, must be accepted or rejected.
basicInformationobjectβœ…Basic user information for the provider. See Provider Basic Information below.
workOnHolidaysboolean❌Whether the provider works on holidays. Defaults to false if not provided.
isCompanyboolean❌Whether the provider is a company. Defaults to false if not provided.
isMainProviderEligibleboolean❌Whether the provider is eligible to be assigned as the main provider. Defaults to true if not provided.
isLeadAssignmentEligibleboolean❌Whether the provider is eligible for lead assignment. Defaults to true if not provided.
service_variantsarray[object]❌Array of service variants associated with this provider. Variants can be referenced by ID or SKU.
availabilityarray[object]❌Array of availability schedules defining when the provider is available.
areasarray[object]❌Array of working areas where the provider operates. Areas can be referenced by ID or reference.

πŸͺͺ Provider Basic Information

FieldTypeRequiredDescription
basicInformation.namestringβœ…Provider's first name.
basicInformation.lastNamestringβœ…Provider's last name.
basicInformation.phoneNumberstringβœ…Provider's phone number.
basicInformation.emailstringβœ…Provider's email address. Must be unique in the system.
basicInformation.identificationNumberstringβœ…Provider's identification number (e.g., national ID, passport).
basicInformation.identificationNumberCountrystringβœ…Country code for the identification number.
basicInformation.referencestring❌Optional reference identifier (separate from provider reference).

πŸ“… Provider Availability

FieldTypeRequiredDescription
availability[].day_of_weeknumberβœ…Day of the week (0 = Sunday, 1 = Monday, 2 = Tuesday, 3 = Wednesday, 4 = Thursday, 5 = Friday, 6 = Saturday).
availability[].start_timestringβœ…Start time in HH:mm format (e.g., "09:00").
availability[].end_timestringβœ…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

FieldTypeRequiredDescription
areas[].typestringβœ…Type of area identifier. Must be one of: "areaId" or "areaReference".
areas[].valuestringβœ…The area ID (if type is "areaId") or area reference (if type is "areaReference").
areas[].variantCoveragearray[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[].typestringβœ… when variantCoverage sentType of variant identifier. Must be one of: "variantId" or "variantSku".
areas[].variantCoverage[].valuestringβœ… when variantCoverage sentThe variant ID (if type is "variantId") or SKU (if type is "variantSku").

When variantCoverage is provided for an area, only the listed variants will be restricted to that specific working area. Variants not listed in any variantCoverage entry retain total coverage (unrestricted).


πŸ”§ Variant

FieldTypeRequiredDescription
skustringβœ…Stock Keeping Unit identifier. Must be unique across all variants. Used to identify existing variants for updates.
namestringβœ…Internal name of the service variant.
isInStorebooleanβœ…Whether this variant is available for in-store services.
isExecutionbooleanβœ…Whether this is an execution variant. Only execution variants (true) are processed by this endpoint. Budget variants (false) are skipped.
parentServiceobjectβœ…Parent service information. If the service or category doesn't exist, it will be created. See Parent Service below.
operationAreasobject❌Operation area configurations with detailed (specific areas) and/or all (default parameters for remaining areas).

🏷️ Parent Service

FieldTypeRequiredDescription
parentService.namestringβœ…Name of the service. Matching is done using normalized string comparison (case-insensitive, accent-insensitive).
parentService.descriptionstring❌Description of the service.
parentService.category.namestringβœ…Name of the category. Matching is done using normalized string comparison (case-insensitive, accent-insensitive).
parentService.category.descriptionstring❌Description of the category.

πŸ—ΊοΈ Operation Areas

FieldTypeRequiredDescription
operationAreas.allobject❌Default parameters applied to all active areas not explicitly covered by detailed. Allows setting default pricing and duration for all remaining areas.
operationAreas.detailedarray[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[].typestringβœ… when detailed sentType of area identifier. Must be one of: "areaId" or "areaReference".
operationAreas.detailed[].valuearray[string]βœ… when detailed sentArray of area identifiers. Areas are automatically grouped by currency for operation variant creation.
operationAreas.detailed[].durationstring❌Duration of the operation in minutes (as a string, e.g., "60").
operationAreas.detailed[].bookingAnticipationHoursnumber❌Number of hours in advance a booking must be made for this operation.
operationAreas.detailed[].pricenumber❌Price for this operation variant.
operationAreas.detailed[].costnumber❌Cost for this operation variant.
operationAreas.detailed[].requireEvidenceboolean❌Whether evidence is required for this operation.
operationAreas.detailed[].requireSignatureboolean❌Whether a signature is required for this operation.
operationAreas.all.durationstring❌Default duration in minutes (as a string, e.g., "60"). Applied to all remaining areas not covered by detailed.
operationAreas.all.bookingAnticipationHoursnumber❌Default number of hours in advance a booking must be made. Applied to all remaining areas not covered by detailed.
operationAreas.all.pricenumber❌Default price. Applied to all remaining areas not covered by detailed.
operationAreas.all.costnumber❌Default cost. Applied to all remaining areas not covered by detailed.
operationAreas.all.requireEvidenceboolean❌Default requirement for evidence. Applied to all remaining areas not covered by detailed.
operationAreas.all.requireSignatureboolean❌Default requirement for signature. Applied to all remaining areas not covered by detailed.

Example Request

json
{
  "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

json
{
  "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

FieldTypeDescription
successbooleanIndicates whether all operations completed successfully.
messagestringHuman-readable message describing the operation result.
variantsProcessednumberNumber of variants successfully processed.
providersProcessednumberNumber of providers successfully processed.
errorsarray[string]Array of error messages if any operations failed. Each message includes the identifier (SKU or reference) and the error description.
variantResultsarray[object]Detailed processing results for each variant.
variantResults[].variantIdnumberThe ID of the variant that was processed.
variantResults[].skustringThe SKU identifier of the variant.
variantResults[].successbooleanWhether this specific variant was processed successfully.
variantResults[].errorstringError message if the variant processing failed. Only present when success is false.
providerResultsarray[object]Detailed processing results for each provider.
providerResults[].providerIdnumberThe ID of the provider that was processed.
providerResults[].referencestringThe reference identifier of the provider.
providerResults[].successbooleanWhether this specific provider was processed successfully.
providerResults[].errorstringError message if the provider processing failed. Only present when success is false.

Error Response Example

json
{
  "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 StatusDescription
400 Bad RequestOne or more required fields are missing or invalid.
401 UnauthorizedAuthentication token missing or expired.
403 ForbiddenThe API key or origin is not authorized.
404 Not FoundA referenced variant, provider, or area could not be found.
500 Internal Server ErrorUnexpected 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: false and no parent relationship)

    • Networks (providers with isCompany: true that act as parent organizations)

  • It MUST NOT be used to create or update technicians that belong to a company (providers with a parent relationship). 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: true are processed. Variants with isExecution: false are skipped with a warning log entry and do not count toward the variantsProcessed total.

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 variantCoverage for 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 in variantCoverage across multiple areas, it is restricted to all of those areas combined.

  • Variants with no variantCoverage entry anywhere have total coverage β€” they operate across all working areas of the provider.

  • A variant listed in variantCoverage but absent from service_variants is automatically added as a new provider-variant association with its area restriction.

Important: if the same variant appears in both service_variants and in a variantCoverage entry, 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_variants is explicitly provided.

  • Only associations absent from both service_variants and all variantCoverage entries 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 areas array are preserved.

  • Remove: Working areas that exist but are not in the incoming areas array are deleted.

  • Add: New working areas are created for areas in the incoming areas array 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 availability array.

professionalServiceVariants (service_variants field)

  • If service_variants is omitted: Existing provider-variant associations are left untouched. Only variantCoverage entries (from areas) may implicitly add new associations or update working-area restrictions on existing ones.

  • If service_variants is provided: The existing associations are fully reconciled against the incoming list:

    • Keep: Associations that match variants in the incoming service_variants array (or variantCoverage entries) are preserved.

    • Remove: Associations absent from both service_variants and all variantCoverage entries are deleted.

    • Add: New associations are created for variants in service_variants or variantCoverage entries that don't have an existing association.

Provider basic information fields (isMainProviderEligible, isLeadAssignmentEligible, workOnHolidays) are updated directly from the payload values. The isCompany field 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

  1. Area Resolution and Currency Grouping

    • Each area value in each detailed entry 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.

  2. 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.

  3. 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

  4. 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

  5. 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)

  1. Remaining Areas Identification

    • The system retrieves all active areas in the system.

    • It filters out areas already processed in Phase 1 (from detailed entries).

  2. Synthetic DTO Creation

    • For the remaining areas, the system creates synthetic area configurations using the parameters from the all configuration.
  3. 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 detailed or implicitly via all) 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: detailed entries take precedence over all. Areas specified in detailed are processed first, and all parameters are only applied to areas not covered by detailed.

Usage Patterns

Pattern 1: Detailed Configuration Only

Use detailed when you need specific configurations for specific areas:

json
{
  "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:

json
{
  "operationAreas": {
    "all": {
      "price": 100.0,
      "duration": "60",
      "cost": 50.0
    }
  }
}

Use both detailed and all when you need specific configurations for some areas and default parameters for the rest:

json
{
  "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:

json
{
  "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 all for remaining areas)