Catalog

Save a whole service group

PUT
/api/v1/service-groups/{id}

Replaces the group in one transaction - its stages, questions, options and entries - as the admin configurator saves it. Every ref is the real id of an existing row of this group or a new one starting with new-; whatever the body does not list is removed, and a stage without label loses its label (as on the admin). A stage with newService creates its service. The answer carries the saved group and the real id of every new- ref. All or nothing; one audit row per save. Draft. An unknown id is 404. PRO plan and above, as on the admin. Requires the catalog:write scope and an Idempotency-Key header.

Authorization

ApiKeyAuth catalog:write
headerAuthorizationBearer <shyni_sk_live_*>

An API key, issued once and never retrievable again: an organization key in the developer portal, or a studio key in the studio's admin (Moje studio → API). Send as Authorization: Bearer shyni_sk_live_....

Scope: catalog:write

Path Parameters

id*string

The group id.

Header Parameters

Idempotency-Key*string

Caller-chosen idempotency token for this write. A retry under the SAME key with the SAME body replays the first response; the SAME key with a DIFFERENT body is 422 IDEMPOTENCY_KEY_MISMATCH. See Idempotency above.

Lengthlength <= 64

Request Body

application/json
  1. body

The whole group. stages, questions and entries are lists of objects with a ref (the real id of an existing row of this group, or a new one starting with new-): stages[]: ref, label, forWhom, addonIds, questions[] (questionRef, required, requiredSentence) and, for a new stage, newService (name, basePrice, durationMinutes, variants, description: plain text or a rich text { "v": 1, "doc": ... } of at most 1000 plain characters, as on the service create; omitted or blank = the service name); questions[]: ref, name, unansweredName, unansweredSentence, options[] (ref, name, sentence, surcharge, scalesWithSize, extraMinutes, isDefault); entries[]: ref, name, lead, stageRef, optionRefs.

name*string
slug?string
categoryId*string
lead?string
scaleTitle?string
imageId?string
inspectionMode?string
Value in"OFF""OPTION""FIRST"
defaultStageRef?string

The ref of the default stage.

stages*array<>
questions?array<>
entries?array<>

Response Body

The saved group and the ids of the created rows.

application/json
  1. response

PUT /service-groups/{id} answer: the saved group and the real id of every new- ref the body created.

group*

A service group (configurator) as the group endpoints show it: the DRAFT rows the studio edits. Every id is the logical id the endpoints take. findings are the structure problems that block POST /catalog/publish.

ids*array<>
Example Request
curl -X PUT 'https://api.shyni.io/api/v1/service-groups/string' \  -H 'Authorization: Bearer shyni_sk_test_YOUR_KEY' \  -H 'Idempotency-Key: 7f3c9a52-1d4e-4b86-a0c7-5e2b9d8f6a13' \  -H 'Content-Type: application/json' \  -d '{  "name": "string",  "categoryId": "string",  "stages": [    {}  ]}'

Response 200

The saved group and the ids of the created rows.

{  "group": {    "id": "string",    "slug": "string",    "name": "string",    "sortOrder": 0,    "categoryId": "string",    "inspectionMode": "OFF",    "defaultServiceId": "string",    "lead": "string",    "scaleTitle": "string",    "imageId": "string",    "stages": [      {        "serviceId": "string",        "name": "string",        "label": "string",        "forWhom": "string",        "groupOrder": 0      }    ],    "questions": [      {        "id": "string",        "groupId": "string",        "name": "string",        "sortOrder": 0,        "unansweredName": "string",        "unansweredSentence": "string",        "options": [          {            "id": "string",            "name": "string",            "sentence": "string",            "surcharge": 0,            "scalesWithSize": true,            "extraMinutes": 0,            "isDefault": true          }        ],        "stageLinks": [          {            "serviceId": "string",            "required": true,            "requiredSentence": "string"          }        ]      }    ],    "entries": [      {        "id": "string",        "name": "string",        "lead": "string",        "serviceId": "string",        "optionIds": [          "string"        ],        "sortOrder": 0      }    ],    "findings": [      {        "code": "string",        "refs": [          "string"        ]      }    ]  },  "ids": [    {      "ref": "string",      "id": "string"    }  ]}

Response 400

A parameter, header or request body is missing, malformed, or out of range. A request body property the endpoint does not define answers UNKNOWN_FIELD instead, with field naming it.

{  "code": "INVALID_INPUT",  "message": "Parameter perPage must be between 1 and 100."}

Response 401

Missing, malformed, unknown, or revoked API key (INVALID_API_KEY).

{  "code": "INVALID_API_KEY",  "message": "Missing or invalid API key."}

Response 403

The key is valid but the request is refused. Branch on code: INSUFFICIENT_SCOPE (the key lacks the permission this endpoint requires), PLAN_REQUIRED (the studio is not on a plan that includes API keys, Pro and above), ORIGIN_NOT_ALLOWED, SECRET_KEY_IN_BROWSER, or DEMO_STUDIO on a write to the demo studio. See Authentication and Scopes above.

{  "code": "INSUFFICIENT_SCOPE",  "message": "This API key does not have the permission this endpoint requires."}

Response 404

The resource does not exist, or belongs to a different studio.

{  "code": "NOT_FOUND",  "message": "Order not found."}

Response 422

The same Idempotency-Key was already used for a request with a different body, use a new key (see Idempotency above).

{  "code": "IDEMPOTENCY_KEY_MISMATCH",  "message": "This Idempotency-Key was already used for a different request. Use a new key."}

Response 429

Burst rate limit exceeded.

{  "code": "RATE_LIMITED",  "message": "Too many requests. Retry after the time given in the Retry-After header."}