{
  "openapi": "3.0.3",
  "info": {
    "title": "Progus Store Locator API",
    "version": "1.0.0",
    "description": "Server-to-server API for the locations shown by Progus Store Locator. Authentication is a bearer API key minted in the app under API access; the API requires the Pro plan or higher. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset for the current minute. Human documentation: https://sl-app.proguscommerce.com/docs/api",
    "contact": {
      "name": "Progus support",
      "url": "https://sl-app.proguscommerce.com/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://sl-app.proguscommerce.com/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    { "name": "Locations", "description": "The rows shown on the merchant's map." },
    { "name": "Batch", "description": "Bulk upserts, processed asynchronously." },
    { "name": "Dictionaries", "description": "The shop's tags, groups and custom fields." },
    { "name": "Account", "description": "Plan and rate limit reporting." }
  ],
  "paths": {
    "/limits": {
      "get": {
        "tags": ["Account"],
        "summary": "Plan, location usage and the current rate limit budgets",
        "operationId": "getLimits",
        "responses": {
          "200": {
            "description": "The current budgets. Reading them spends a request but no write budget.",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Limits" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/locations": {
      "get": {
        "tags": ["Locations"],
        "summary": "List locations",
        "description": "Keyset pagination. Follow next_cursor until it is null; a null next_cursor means this was the last page.",
        "operationId": "listLocations",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Rows per page, 1 to 200. Defaults to 50.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The next_cursor of the previous page. Opaque; do not construct one.",
            "required": false,
            "schema": { "type": "string" }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Only locations changed at or after this ISO 8601 instant, e.g. 2026-09-08T00:00:00Z.",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "active",
            "in": "query",
            "description": "Only active (true) or only inactive (false) locations.",
            "required": false,
            "schema": { "type": "boolean" }
          },
          {
            "name": "external_id",
            "in": "query",
            "description": "The one location carrying this external id, if the shop has it.",
            "required": false,
            "schema": { "type": "string", "maxLength": 64 }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of locations.",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LocationPage" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "post": {
        "tags": ["Locations"],
        "summary": "Create a location",
        "operationId": "createLocation",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LocationCreate" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. A repeat with the same Idempotency-Key replays this body instead of creating a second location.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LocationWriteResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PlanLimitReached" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/locations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The location's numeric id, as returned by this API.",
          "schema": { "type": "integer", "format": "int64" }
        }
      ],
      "get": {
        "tags": ["Locations"],
        "summary": "Read one location",
        "operationId": "getLocation",
        "responses": {
          "200": {
            "description": "The location.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LocationResult" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "patch": {
        "tags": ["Locations"],
        "summary": "Update some fields of a location",
        "description": "Only the fields present in the body are changed. Sending lat as null asks for the address to be geocoded again.",
        "operationId": "patchLocation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LocationPatch" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The location after the change.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LocationWriteResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "delete": {
        "tags": ["Locations"],
        "summary": "Delete a location",
        "description": "Idempotent: a location that is already gone is still a 204.",
        "operationId": "deleteLocation",
        "responses": {
          "204": { "description": "Deleted, or already absent." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/locations/by-external-id/{external_id}": {
      "parameters": [
        {
          "name": "external_id",
          "in": "path",
          "required": true,
          "description": "Your own identifier for this location, 1 to 64 characters, unique within the shop.",
          "schema": { "type": "string", "minLength": 1, "maxLength": 64 }
        }
      ],
      "put": {
        "tags": ["Locations"],
        "summary": "Create or replace a location by your own id",
        "description": "The endpoint a CRM or ERP syncs through: no bookkeeping of our ids, no lookup before the write. 201 when the location was created, 200 when an existing one was updated.",
        "operationId": "upsertLocationByExternalId",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LocationUpsert" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An existing location was updated.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LocationWriteResult" }
              }
            }
          },
          "201": {
            "description": "A location was created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LocationWriteResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PlanLimitReached" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "delete": {
        "tags": ["Locations"],
        "summary": "Delete a location by your own id",
        "description": "Idempotent: an external id that is not in the shop is still a 204.",
        "operationId": "deleteLocationByExternalId",
        "responses": {
          "204": { "description": "Deleted, or already absent." },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/locations/batch": {
      "post": {
        "tags": ["Batch"],
        "summary": "Upsert up to 500 locations as one background job",
        "description": "Every item is an upsert on external_id. The request is validated in full and answered with 202 and a job id; nothing is written before the worker picks the job up. One job per shop runs at a time - a second request while one is queued or running is a 409 carrying the id of the job to wait for.",
        "operationId": "createLocationBatch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BatchRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Poll GET /jobs/{id} for progress.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchAccepted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/BatchItemsInvalid" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/jobs/{id}": {
      "get": {
        "tags": ["Batch"],
        "summary": "The status of a batch job",
        "operationId": "getJob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The job_id returned by POST /locations/batch.",
            "schema": { "type": "integer", "format": "int64" }
          }
        ],
        "responses": {
          "200": {
            "description": "The job. Terminal statuses are succeeded, partial and failed.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobResult" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/custom-fields": {
      "get": {
        "tags": ["Dictionaries"],
        "summary": "The shop's custom fields",
        "description": "The ids used as the keys of a location's custom_fields object.",
        "operationId": "listCustomFields",
        "responses": {
          "200": {
            "description": "Every custom field this shop has defined.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CustomFieldList" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/tags": {
      "get": {
        "tags": ["Dictionaries"],
        "summary": "The shop's tags",
        "description": "The ids accepted in a location's tag_ids.",
        "operationId": "listTags",
        "responses": {
          "200": {
            "description": "Every tag this shop has defined.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TagList" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/groups": {
      "get": {
        "tags": ["Dictionaries"],
        "summary": "The shop's groups",
        "operationId": "listGroups",
        "responses": {
          "200": {
            "description": "Every group this shop has defined.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GroupList" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer sl_live_... - the key shown once when it was created in the app under API access. Keys are not accepted in a query string."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Up to 255 characters of your own. A repeat of the same key within 24 hours replays the first response instead of creating a second location; a repeat while the first request is still running is a 409.",
        "schema": { "type": "string", "maxLength": 255 }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Requests allowed in the current minute.",
        "schema": { "type": "integer" }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current minute.",
        "schema": { "type": "integer" }
      },
      "RateLimitReset": {
        "description": "Seconds until the current minute's window resets.",
        "schema": { "type": "integer" }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": { "type": "integer" }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The shape of every error this API returns.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable and never localised - the part to branch on.",
                "enum": [
                  "api_disabled",
                  "api_not_provisioned",
                  "unauthorized",
                  "forbidden_scope",
                  "plan_required",
                  "rate_limited",
                  "validation_failed",
                  "not_found",
                  "conflict",
                  "plan_limit_reached",
                  "internal"
                ]
              },
              "message": { "type": "string", "description": "A fixed English sentence, for a human reading a log." },
              "fields": {
                "type": "array",
                "description": "Present for validation failures only.",
                "items": { "$ref": "#/components/schemas/FieldError" }
              }
            }
          }
        }
      },
      "FieldError": {
        "type": "object",
        "properties": {
          "field": { "type": "string", "description": "The field that was refused, e.g. \"name\" or \"opening_hours.periods\"." },
          "code": { "type": "string", "description": "One of required, type, too_long, not_allowed, not_found." },
          "message": { "type": "string" }
        }
      },
      "BatchItemError": {
        "type": "object",
        "properties": {
          "index": { "type": "integer", "description": "The position of the item in the items array." },
          "errors": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/FieldError" }
          }
        }
      },
      "OpeningHoursInput": {
        "type": "object",
        "description": "Structured opening hours. Send null to clear them.",
        "required": ["timezone", "periods"],
        "properties": {
          "timezone": { "type": "string", "description": "An IANA timezone, e.g. \"Europe/Warsaw\". Required." },
          "periods": {
            "type": "array",
            "description": "At most one entry per day; days not listed are treated as not set.",
            "items": {
              "type": "object",
              "required": ["day"],
              "properties": {
                "day": { "type": "integer", "minimum": 0, "maximum": 6, "description": "0 is Sunday, 6 is Saturday." },
                "open": { "type": "string", "description": "\"HH:MM\", 24-hour. Required unless closed is true." },
                "close": { "type": "string", "description": "\"HH:MM\", 24-hour. Required unless closed is true." },
                "closed": { "type": "boolean", "description": "True for a day the location is shut." }
              }
            }
          }
        }
      },
      "Location": {
        "type": "object",
        "description": "A location as this API returns it.",
        "properties": {
          "id": { "type": "integer", "format": "int64", "description": "Our id for the location." },
          "progus_id": { "type": "string", "description": "The obfuscated id used in storefront links." },
          "external_id": { "type": "string", "nullable": true, "description": "Your own id, unique within the shop." },
          "name": { "type": "string", "nullable": true },
          "active": { "type": "boolean", "description": "Whether the location is shown on the storefront." },
          "priority": { "type": "integer", "nullable": true, "description": "Sort weight; higher sorts earlier." },
          "lat": { "type": "number", "format": "double", "nullable": true },
          "lon": { "type": "number", "format": "double", "nullable": true },
          "place_id": { "type": "string", "nullable": true },
          "address": { "type": "string", "nullable": true },
          "full_address": { "type": "string", "nullable": true },
          "city": { "type": "string", "nullable": true },
          "province": { "type": "string", "nullable": true },
          "zip_code": { "type": "string", "nullable": true },
          "country": { "type": "string", "nullable": true },
          "phone": { "type": "string", "nullable": true },
          "fax": { "type": "string", "nullable": true },
          "email": { "type": "string", "nullable": true },
          "website": { "type": "string", "nullable": true },
          "image_url": { "type": "string", "nullable": true },
          "info": { "type": "string", "nullable": true },
          "facebook": { "type": "string", "nullable": true },
          "google": { "type": "string", "nullable": true },
          "linkedin": { "type": "string", "nullable": true },
          "instagram": { "type": "string", "nullable": true },
          "twitter": { "type": "string", "nullable": true },
          "youtube": { "type": "string", "nullable": true },
          "whatsapp": { "type": "string", "nullable": true, "description": "A phone number, which the storefront turns into a wa.me link, or a full URL." },
          "marker_id": { "type": "integer", "nullable": true, "description": "The marker style this location uses, if it is not the shop default." },
          "opening_hours": {
            "nullable": true,
            "description": "Structured hours as {format, timezone, periods}, or the free text a merchant typed in the admin before structured hours existed."
          },
          "custom_fields": {
            "type": "object",
            "description": "Custom field id (as a string key) to value. See GET /custom-fields.",
            "additionalProperties": { "type": "string" }
          },
          "tag_ids": {
            "type": "array",
            "description": "See GET /tags.",
            "items": { "type": "integer" }
          },
          "created_at": { "type": "string", "format": "date-time", "nullable": true },
          "updated_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "LocationInput": {
        "type": "object",
        "description": "The writable fields of a location. Text fields are at most 255 characters, info at most 65535.",
        "properties": {
          "name": { "type": "string", "maxLength": 255 },
          "active": { "type": "boolean" },
          "priority": { "type": "integer", "minimum": 0, "maximum": 1000000 },
          "lat": { "type": "number", "format": "double", "minimum": -90, "maximum": 90, "nullable": true, "description": "Send both lat and lon to set coordinates yourself. Omit them and the address is geocoded for you." },
          "lon": { "type": "number", "format": "double", "minimum": -180, "maximum": 180, "nullable": true },
          "place_id": { "type": "string", "maxLength": 255, "nullable": true },
          "address": { "type": "string", "maxLength": 255, "nullable": true },
          "full_address": { "type": "string", "maxLength": 255, "nullable": true },
          "city": { "type": "string", "maxLength": 255, "nullable": true },
          "province": { "type": "string", "maxLength": 255, "nullable": true },
          "zip_code": { "type": "string", "maxLength": 255, "nullable": true },
          "country": { "type": "string", "maxLength": 255, "nullable": true },
          "phone": { "type": "string", "maxLength": 255, "nullable": true },
          "fax": { "type": "string", "maxLength": 255, "nullable": true },
          "email": { "type": "string", "maxLength": 255, "nullable": true },
          "website": { "type": "string", "maxLength": 255, "nullable": true },
          "image_url": { "type": "string", "maxLength": 255, "nullable": true },
          "info": { "type": "string", "maxLength": 65535, "nullable": true },
          "facebook": { "type": "string", "maxLength": 255, "nullable": true },
          "google": { "type": "string", "maxLength": 255, "nullable": true },
          "linkedin": { "type": "string", "maxLength": 255, "nullable": true },
          "instagram": { "type": "string", "maxLength": 255, "nullable": true },
          "twitter": { "type": "string", "maxLength": 255, "nullable": true },
          "youtube": { "type": "string", "maxLength": 255, "nullable": true },
          "whatsapp": { "type": "string", "maxLength": 255, "nullable": true, "description": "A phone number, which the storefront turns into a wa.me link, or a full URL." },
          "opening_hours": { "$ref": "#/components/schemas/OpeningHoursInput" },
          "custom_fields": {
            "type": "object",
            "description": "Custom field id (as a string key) to value. Fields you do not mention keep the value they had.",
            "additionalProperties": { "type": "string", "nullable": true }
          },
          "tag_ids": {
            "type": "array",
            "maxItems": 200,
            "description": "Replaces the location's tags. Every id must be one of the shop's own - see GET /tags.",
            "items": { "type": "integer", "minimum": 1 }
          }
        }
      },
      "LocationCreate": {
        "allOf": [
          { "$ref": "#/components/schemas/LocationInput" },
          {
            "type": "object",
            "required": ["name"],
            "properties": {
              "external_id": { "type": "string", "maxLength": 64, "nullable": true, "description": "Your own id. Must not already be used by another location in this shop." }
            }
          }
        ]
      },
      "LocationUpsert": {
        "allOf": [
          { "$ref": "#/components/schemas/LocationInput" },
          {
            "type": "object",
            "required": ["name"],
            "description": "external_id is in the path and is not a body field here."
          }
        ]
      },
      "LocationPatch": {
        "allOf": [
          { "$ref": "#/components/schemas/LocationInput" },
          {
            "type": "object",
            "description": "Nothing is required; only the fields present are changed. name may be changed but not cleared.",
            "properties": {
              "external_id": { "type": "string", "maxLength": 64, "nullable": true }
            }
          }
        ]
      },
      "LocationResult": {
        "type": "object",
        "properties": {
          "data": { "$ref": "#/components/schemas/Location" }
        }
      },
      "LocationWriteResult": {
        "type": "object",
        "properties": {
          "data": { "$ref": "#/components/schemas/Location" },
          "geocoding": {
            "type": "string",
            "description": "What is happening to this location's coordinates. \"pending\" - a lookup is queued and coordinates appear within minutes. \"not_needed\" - it already has usable coordinates, or nothing to look up. \"skipped_quota\" - the shop is out of geocoding budget for today; the location is written without coordinates and can be geocoded from the app or re-sent tomorrow.",
            "enum": ["pending", "not_needed", "skipped_quota"]
          }
        }
      },
      "LocationPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Location" }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Pass as ?cursor= for the next page. Null on the last page."
          }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "description": "Each item is an upsert on its external_id, which is required here.",
            "items": {
              "allOf": [
                { "$ref": "#/components/schemas/LocationInput" },
                {
                  "type": "object",
                  "required": ["name", "external_id"],
                  "properties": {
                    "external_id": { "type": "string", "minLength": 1, "maxLength": 64 }
                  }
                }
              ]
            }
          }
        }
      },
      "BatchAccepted": {
        "type": "object",
        "properties": {
          "job_id": { "type": "integer", "format": "int64" },
          "status": { "type": "string", "enum": ["queued"] },
          "accepted_count": { "type": "integer", "description": "How many items were taken. Nothing is written yet." }
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "type": { "type": "string", "enum": ["locations.batch"] },
          "status": {
            "type": "string",
            "description": "queued and running are in progress; succeeded, partial and failed are terminal.",
            "enum": ["queued", "running", "succeeded", "partial", "failed"]
          },
          "total_count": { "type": "integer" },
          "processed_count": { "type": "integer", "description": "Items applied so far, successful ones only." },
          "failed_count": { "type": "integer" },
          "errors": {
            "type": "array",
            "description": "The first failures, with the index of the item in the original request.",
            "items": { "$ref": "#/components/schemas/BatchItemError" }
          },
          "created_at": { "type": "string", "format": "date-time", "nullable": true },
          "started_at": { "type": "string", "format": "date-time", "nullable": true },
          "finished_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "JobResult": {
        "type": "object",
        "properties": {
          "data": { "$ref": "#/components/schemas/Job" }
        }
      },
      "Limits": {
        "type": "object",
        "properties": {
          "plan": { "type": "string", "description": "The shop's current plan, e.g. \"pro\"." },
          "locations": {
            "type": "object",
            "properties": {
              "limit": { "type": "integer", "description": "How many locations the plan allows." },
              "used": { "type": "integer" },
              "remaining": { "type": "integer", "description": "Never below zero; a shop that downgraded can be over its limit." }
            }
          },
          "rateLimits": {
            "type": "object",
            "properties": {
              "requestsPerMinute": {
                "type": "object",
                "properties": {
                  "limit": { "type": "integer" },
                  "remaining": { "type": "integer" },
                  "resetSeconds": { "type": "integer" }
                }
              },
              "writesPerDay": {
                "type": "object",
                "properties": {
                  "limit": { "type": "integer" },
                  "remaining": { "type": "integer", "nullable": true, "description": "Null when the counter could not be read." },
                  "resetSeconds": { "type": "integer" }
                }
              }
            }
          }
        }
      },
      "CustomFieldList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "integer" },
                "name": { "type": "string", "nullable": true },
                "type": { "type": "integer", "nullable": true },
                "show_label": { "type": "boolean" }
              }
            }
          }
        }
      },
      "TagList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "integer" },
                "name": { "type": "string", "nullable": true },
                "active": { "type": "boolean" },
                "active_on_form": { "type": "boolean" },
                "priority": { "type": "integer", "nullable": true }
              }
            }
          }
        }
      },
      "GroupList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "integer" },
                "name": { "type": "string", "nullable": true },
                "priority": { "type": "integer", "nullable": true }
              }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No key, a malformed key, an unknown key or a revoked key - the answer is the same for all four.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Forbidden": {
        "description": "plan_required - the shop is below Pro; or forbidden_scope - the key does not carry the scope this operation needs.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "not_found. A location belonging to another shop is reported exactly like one that never existed.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Conflict": {
        "description": "conflict - the external_id is already taken by another location, an Idempotency-Key is still in flight, or the shop already has a batch job that has not finished.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ValidationFailed": {
        "description": "validation_failed. `fields` names what was refused.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "BatchItemsInvalid": {
        "description": "validation_failed for a well formed batch whose items are not all valid. Nothing was written; `items` carries the index of each bad item.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "PlanLimitReached": {
        "description": "plan_limit_reached - the shop already holds as many locations as its plan allows.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "RateLimited": {
        "description": "rate_limited - too many requests this minute, or too many writes today.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Unavailable": {
        "description": "api_disabled or api_not_provisioned - the API is not serving this installation yet. Retry later.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    }
  }
}
