{
  "components": {
    "schemas": {
      "CreateContactDto": {
        "properties": {
          "birthDate": {
            "description": "Изисква scope contacts:pii.",
            "format": "date",
            "type": "string"
          },
          "egn": {
            "description": "Изисква scope contacts:pii.",
            "type": "string"
          },
          "eik": {
            "description": "ЕИК — само цифри.",
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "idCardNumber": {
            "description": "Изисква scope contacts:pii.",
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "legalName": {
            "type": "string"
          },
          "marketingConsent": {
            "default": false,
            "type": "boolean"
          },
          "middleName": {
            "type": "string"
          },
          "name": {
            "description": "Име на фирмата (за type=LEGAL_ENTITY).",
            "type": "string"
          },
          "passportNumber": {
            "description": "Изисква scope contacts:pii.",
            "type": "string"
          },
          "salutation": {
            "type": "string"
          },
          "type": {
            "enum": [
              "INDIVIDUAL",
              "LEGAL_ENTITY"
            ],
            "type": "string"
          },
          "vatNumber": {
            "type": "string"
          }
        },
        "required": [
          "type"
        ],
        "type": "object"
      },
      "CreateReservationDto": {
        "properties": {
          "adults": {
            "maximum": 99,
            "minimum": 1,
            "type": "number"
          },
          "children": {
            "default": 0,
            "maximum": 99,
            "minimum": 0,
            "type": "number"
          },
          "childrenAges": {
            "description": "Възрасти на децата.",
            "items": {
              "type": "number"
            },
            "type": "array"
          },
          "contact": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ReservationContactDto"
              }
            ],
            "description": "Данни за нов контакт, ако не е подаден contactId."
          },
          "contactId": {
            "format": "uuid",
            "type": "string"
          },
          "departureDate": {
            "format": "date",
            "type": "string"
          },
          "marketingConsent": {
            "default": false,
            "type": "boolean"
          },
          "notes": {
            "type": "string"
          },
          "programId": {
            "description": "Програма от каталога.",
            "format": "uuid",
            "type": "string"
          },
          "returnDate": {
            "format": "date",
            "type": "string"
          }
        },
        "required": [
          "programId",
          "adults"
        ],
        "type": "object"
      },
      "PublicApiErrorBodyDto": {
        "properties": {
          "code": {
            "description": "Stable machine-readable code. Branch on this, never on the message.",
            "example": "VALIDATION_FAILED",
            "type": "string"
          },
          "details": {
            "description": "Present only when specific fields can be named.",
            "items": {
              "$ref": "#/components/schemas/PublicApiErrorDetailDto"
            },
            "type": "array"
          },
          "message": {
            "description": "Human-readable explanation. May be reworded at any time.",
            "type": "string"
          },
          "requestId": {
            "description": "Quote this when contacting support.",
            "example": "req_8f21c0",
            "type": "string"
          }
        },
        "required": [
          "code",
          "message",
          "requestId"
        ],
        "type": "object"
      },
      "PublicApiErrorDetailDto": {
        "properties": {
          "field": {
            "description": "The offending field, in dot notation.",
            "type": "string"
          },
          "issue": {
            "description": "What is wrong with it.",
            "type": "string"
          }
        },
        "required": [
          "field",
          "issue"
        ],
        "type": "object"
      },
      "PublicApiErrorDto": {
        "properties": {
          "error": {
            "$ref": "#/components/schemas/PublicApiErrorBodyDto"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "PublicContactDto": {
        "properties": {
          "birthDate": {
            "description": "Requires the contacts:pii scope.",
            "format": "date",
            "nullable": true,
            "type": "string"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "egn": {
            "description": "Requires the contacts:pii scope.",
            "nullable": true,
            "type": "string"
          },
          "eik": {
            "description": "Bulgarian company identifier.",
            "nullable": true,
            "type": "string"
          },
          "firstName": {
            "nullable": true,
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "idCardNumber": {
            "description": "Requires the contacts:pii scope.",
            "nullable": true,
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "lastName": {
            "nullable": true,
            "type": "string"
          },
          "legalName": {
            "nullable": true,
            "type": "string"
          },
          "lifecycleStage": {
            "type": "string"
          },
          "marketingConsent": {
            "type": "boolean"
          },
          "middleName": {
            "nullable": true,
            "type": "string"
          },
          "name": {
            "description": "Company display name; null for individuals.",
            "nullable": true,
            "type": "string"
          },
          "passportNumber": {
            "description": "Requires the contacts:pii scope.",
            "nullable": true,
            "type": "string"
          },
          "salutation": {
            "example": "г-н",
            "nullable": true,
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "type": {
            "enum": [
              "individual",
              "company"
            ],
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          },
          "vatNumber": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "id",
          "type",
          "salutation",
          "firstName",
          "middleName",
          "lastName",
          "name",
          "legalName",
          "eik",
          "vatNumber",
          "status",
          "lifecycleStage",
          "isActive",
          "marketingConsent",
          "createdAt",
          "updatedAt"
        ],
        "type": "object"
      },
      "PublicContactListDto": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PublicContactDto"
            },
            "type": "array"
          },
          "nextCursor": {
            "description": "Cursor for the next page, or null when the list is exhausted.",
            "format": "uuid",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "data",
          "nextCursor"
        ],
        "type": "object"
      },
      "PublicProgramDto": {
        "properties": {
          "comfortLevel": {
            "description": "1–5, по преценка на оператора.",
            "nullable": true,
            "type": "number"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "durationDays": {
            "type": "number"
          },
          "guideIncluded": {
            "type": "boolean"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "isEarlyBird": {
            "type": "boolean"
          },
          "isHotOffer": {
            "type": "boolean"
          },
          "isLastMinute": {
            "type": "boolean"
          },
          "metaDescription": {
            "nullable": true,
            "type": "string"
          },
          "minPrice": {
            "description": "Entry price for the programme, or null when none is published. Money is a number here and everywhere else in the contract.",
            "nullable": true,
            "type": "number"
          },
          "minTourists": {
            "nullable": true,
            "type": "number"
          },
          "name": {
            "type": "string"
          },
          "nightTravels": {
            "type": "number"
          },
          "ogImageUrl": {
            "nullable": true,
            "type": "string"
          },
          "otherCountryCodes": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "ownTransportAllowed": {
            "type": "boolean"
          },
          "primaryCountryCode": {
            "description": "ISO 3166-1 alpha-2.",
            "type": "string"
          },
          "programType": {
            "type": "string"
          },
          "promoLabel": {
            "nullable": true,
            "type": "string"
          },
          "routeDescription": {
            "nullable": true,
            "type": "string"
          },
          "shortTitle": {
            "nullable": true,
            "type": "string"
          },
          "slug": {
            "nullable": true,
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "tagline": {
            "nullable": true,
            "type": "string"
          },
          "transferIncluded": {
            "type": "boolean"
          },
          "transportType": {
            "type": "string"
          },
          "travelType": {
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          },
          "usefulInfo": {
            "description": "Полезна информация за пътуването.",
            "items": {
              "properties": {
                "text": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "nullable": true,
            "type": "array"
          }
        },
        "required": [
          "id",
          "name",
          "shortTitle",
          "tagline",
          "slug",
          "programType",
          "transportType",
          "travelType",
          "durationDays",
          "nightTravels",
          "minTourists",
          "primaryCountryCode",
          "otherCountryCodes",
          "routeDescription",
          "comfortLevel",
          "guideIncluded",
          "transferIncluded",
          "ownTransportAllowed",
          "promoLabel",
          "isHotOffer",
          "isLastMinute",
          "isEarlyBird",
          "currency",
          "minPrice",
          "status",
          "isActive",
          "usefulInfo",
          "metaDescription",
          "ogImageUrl",
          "createdAt",
          "updatedAt"
        ],
        "type": "object"
      },
      "PublicProgramListDto": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PublicProgramDto"
            },
            "type": "array"
          },
          "nextCursor": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "data",
          "nextCursor"
        ],
        "type": "object"
      },
      "PublicReservationDto": {
        "properties": {
          "adults": {
            "type": "number"
          },
          "children": {
            "type": "number"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "departureDate": {
            "format": "date",
            "nullable": true,
            "type": "string"
          },
          "dispatchOutcome": {
            "description": "Резултат от изпращането към доставчика.",
            "nullable": true,
            "type": "string"
          },
          "dispatchedAt": {
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "paymentStatus": {
            "type": "string"
          },
          "referenceNumber": {
            "type": "string"
          },
          "status": {
            "nullable": true,
            "type": "string"
          },
          "supplierReference": {
            "description": "Референция на резервацията при доставчика.",
            "nullable": true,
            "type": "string"
          },
          "totalDiscountEur": {
            "type": "number"
          },
          "totalPaid": {
            "nullable": true,
            "type": "number"
          },
          "totalSellPrice": {
            "description": "Продажна цена към клиента.",
            "type": "number"
          },
          "tourists": {
            "description": "Изисква scope reservations:pii.",
            "items": {
              "$ref": "#/components/schemas/PublicReservationTouristDto"
            },
            "type": "array"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "id",
          "referenceNumber",
          "status",
          "paymentStatus",
          "departureDate",
          "adults",
          "children",
          "totalSellPrice",
          "totalPaid",
          "totalDiscountEur",
          "currency",
          "supplierReference",
          "dispatchOutcome",
          "dispatchedAt",
          "createdAt",
          "updatedAt"
        ],
        "type": "object"
      },
      "PublicReservationListDto": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/PublicReservationDto"
            },
            "type": "array"
          },
          "nextCursor": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "data",
          "nextCursor"
        ],
        "type": "object"
      },
      "PublicReservationTouristDto": {
        "properties": {
          "firstName": {
            "nullable": true,
            "type": "string"
          },
          "isPrincipal": {
            "type": "boolean"
          },
          "lastName": {
            "nullable": true,
            "type": "string"
          },
          "middleName": {
            "nullable": true,
            "type": "string"
          },
          "touristNumber": {
            "type": "number"
          }
        },
        "required": [
          "touristNumber",
          "isPrincipal",
          "firstName",
          "middleName",
          "lastName"
        ],
        "type": "object"
      },
      "ReservationContactDto": {
        "properties": {
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "UpdateContactDto": {
        "properties": {
          "birthDate": {
            "description": "Изисква scope contacts:pii.",
            "format": "date",
            "type": "string"
          },
          "egn": {
            "description": "Изисква scope contacts:pii.",
            "type": "string"
          },
          "eik": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "idCardNumber": {
            "description": "Изисква scope contacts:pii.",
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "legalName": {
            "type": "string"
          },
          "marketingConsent": {
            "type": "boolean"
          },
          "middleName": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "passportNumber": {
            "description": "Изисква scope contacts:pii.",
            "type": "string"
          },
          "salutation": {
            "type": "string"
          },
          "vatNumber": {
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "apiKey": {
        "bearerFormat": "erp_live_…",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "contact": {
      "email": "support@tibs.bg",
      "name": "tibs support",
      "url": "https://api.tibs.bg"
    },
    "description": "Programmatic access to a tenant's own ERP data: the programme catalogue,\ncontacts and reservations. The API is consumed by the agency's own systems —\nbooking front-ends, back-office tooling, partner integrations.\n\n## Base URL\n\n```\nhttps://erp.tibs.bg/api/public/v1\n```\n\nThe version is part of the path. It changes only when the contract breaks; see\n**Versioning** below.\n\n## Authentication\n\nEvery request carries a tenant API key as a bearer token:\n\n```\nAuthorization: Bearer erp_live_…\n```\n\nKeys are issued by the agency from the client portal and are shown once — only a\nhash is stored, so a lost key is replaced, not recovered. The prefix marks the\nenvironment: `erp_live_` for real data, `erp_test_` for test data.\n\nTo rotate without downtime, issue the new key, deploy it, confirm it works, then\nrevoke the old one. Several keys may be active at once. Revocation takes effect\non the next request.\n\nThree conditions must hold simultaneously, and each fails with its own code: a\nvalid key, an approved source IP, and the `public_api` add-on enabled for the\ntenant.\n\n## IP allowlist\n\nA key works only from addresses the agency has registered and tibs has approved\nby hand. Requests from anywhere else are refused with `IP_NOT_ALLOWED`,\nregardless of how valid the key is.\n\nRegister the range your hosting provider gives you rather than a single address.\nShared hosting rotates outbound addresses, and a single-address entry will fail\nintermittently.\n\n## Scopes\n\nA key carries an explicit list of scopes. A missing scope is refused with\n`INSUFFICIENT_SCOPE` — never silently reduced to an empty result, which would\nbe indistinguishable from having no data.\n\n| Scope | Grants |\n| --- | --- |\n| `catalog:read` | The programme catalogue |\n| `contacts:read` | Reading contacts |\n| `contacts:write` | Creating and updating contacts |\n| `contacts:pii` | Personal identifiers on a contact — national ID, documents, date of birth. Granted separately and by exception. |\n| `reservations:read` | Reading reservations |\n| `reservations:write` | Creating reservations |\n| `reservations:pii` | Traveller names on a reservation. Without it they are not read from the database at all. |\n\n## Rate limiting\n\nReads and writes draw on separate budgets, so bulk extraction cannot exhaust the\nright to create a reservation.\n\n| Plan | Reads | Writes |\n| --- | --- | --- |\n| FREE | 30 / min · 5 000 / month | 10 / min · 1 000 / month |\n| PRO | 300 / min · 100 000 / month | 60 / min · 20 000 / month |\n\nExceeding a budget returns `429 RATE_LIMITED` with a `Retry-After` header in\nseconds. Wait that long; do not retry in a loop.\n\n## Idempotency\n\nEvery write requires an `Idempotency-Key` header carrying a UUID:\n\n```\nIdempotency-Key: 9f1b6d3e-4c2a-4f8b-9d1e-2a3b4c5d6e7f\n```\n\nReusing the key after a timeout returns the original response instead of\ncreating a second record. Keys are held for 24 hours and are bound to the exact\nrequest body — reusing one with different content is refused with\n`IDEMPOTENCY_CONFLICT`. A request still in flight returns\n`IDEMPOTENCY_IN_PROGRESS`; a request that failed releases its key, so the\nretry can reuse it.\n\n## Concurrency\n\nUpdates require the `If-Match` header carrying the ETag from the last read.\nIf the resource changed in between, the update is refused with\n`PRECONDITION_FAILED` rather than overwriting the newer version.\n\n## Pagination\n\nCollections are cursor-paginated. Pass the `nextCursor` from the previous\nresponse as `cursor`; a `null` cursor means the collection is exhausted.\n`limit` defaults to 50 and is capped at 100. Results are ordered by identifier,\nso a page never shifts under an in-progress traversal.\n\n`updatedSince` narrows a collection to records touched after an ISO timestamp,\nwhich is what an incremental sync should use rather than re-reading everything.\n\n## Guides\n\n### Booking a trip\n\nThree calls: find the programme, create the reservation, read it back.\n\nStart from the catalogue. `minPrice` is an entry price, not the price of a\nspecific date — the reservation is priced server-side from the departure.\n\n```bash\ncurl \"$BASE/programs?limit=5\" -H \"Authorization: Bearer $KEY\"\n```\n\n```json\n{ \"data\": [ { \"id\": \"0f5c2b8a-…\", \"name\": \"Classic Greece\",\n              \"durationDays\": 5, \"minPrice\": 349, \"currency\": \"EUR\" } ],\n  \"nextCursor\": null }\n```\n\nCreate the reservation. Send the traveller as `contact` and the API will reuse a\ncontact with the same e-mail or phone if the agency already has one, rather than\ncreating a duplicate. If you already know the contact, send `contactId` instead.\n\n```bash\ncurl -X POST \"$BASE/reservations\"   -H \"Authorization: Bearer $KEY\"   -H \"Idempotency-Key: $(uuidgen)\"   -H \"Content-Type: application/json\"   -d '{\n    \"programId\": \"0f5c2b8a-…\",\n    \"departureDate\": \"2026-09-15\",\n    \"adults\": 2, \"children\": 1, \"childrenAges\": [9],\n    \"contact\": { \"name\": \"Ivan Petrov\", \"email\": \"ivan@example.com\",\n                 \"phone\": \"+359888123456\" },\n    \"marketingConsent\": true\n  }'\n```\n\n```json\n{ \"id\": \"7c1e9a44-…\", \"referenceNumber\": \"2609123456\",\n  \"status\": \"Pending\", \"paymentStatus\": \"UNPAID\",\n  \"totalSellPrice\": 1047, \"currency\": \"EUR\",\n  \"supplierReference\": null, \"dispatchOutcome\": null }\n```\n\nStore the `id`. `referenceNumber` is what the traveller quotes; it is not an\nidentifier you should look records up by.\n\nThe reservation exists in the agency's ERP from this moment, pending, and is\n**not** sent to the supplier — dispatching is irreversible towards a third party\nand has its own endpoint. `supplierReference` and `dispatchOutcome` stay null\nuntil that happens.\n\nPoll the reservation to follow it. An agent confirms, prices and dispatches it\non their side; `status`, `paymentStatus` and `totalPaid` move accordingly.\n\n```bash\ncurl \"$BASE/reservations/7c1e9a44-…\" -H \"Authorization: Bearer $KEY\"\n```\n\nPoll on the order of minutes, not seconds — nothing here changes faster than a\nhuman works. For a list of what moved recently, use `updatedSince` on the\ncollection instead of polling each reservation.\n\n### Keeping a local copy in sync\n\nRead the whole collection once, then ask only for what changed.\n\n```bash\n# First pass, page by page\ncurl \"$BASE/contacts?limit=100\" -H \"Authorization: Bearer $KEY\"\n# → { \"data\": [ … ], \"nextCursor\": \"9d3f…\" }\ncurl \"$BASE/contacts?limit=100&cursor=9d3f…\" -H \"Authorization: Bearer $KEY\"\n# → { \"data\": [ … ], \"nextCursor\": null }   ← collection exhausted\n```\n\nRecord the timestamp of the run, then use it:\n\n```bash\ncurl \"$BASE/contacts?updatedSince=2026-09-15T00:00:00Z&limit=100\"   -H \"Authorization: Bearer $KEY\"\n```\n\nRecords are ordered by identifier, so a page never shifts under an in-progress\ntraversal. Re-reading the whole collection on a schedule is what exhausts a\nmonth's budget in a week; `updatedSince` is what the limits are sized for.\n\nUpdating a contact requires the version you read. Take the `ETag` from the\nresponse and send it back in `If-Match`:\n\n```bash\ncurl -X PATCH \"$BASE/contacts/5b2c…\"   -H \"Authorization: Bearer $KEY\"   -H 'If-Match: \"1758888000000\"'   -H \"Content-Type: application/json\"   -d '{ \"marketingConsent\": false }'\n```\n\nIf someone changed the contact in the ERP in the meantime, this returns\n`PRECONDITION_FAILED` instead of overwriting their work. Re-read, reapply,\nretry.\n\n### Failing safely\n\nRetry on `429` and `5xx`. Do not retry on `4xx` — the request will fail the same\nway until it is changed.\n\n| Code | What it means for a retry |\n| --- | --- |\n| `RATE_LIMITED` | Wait the seconds in `Retry-After`, then retry the same request. |\n| `IDEMPOTENCY_IN_PROGRESS` | The first attempt is still running. Wait a few seconds, retry with the same key. |\n| `INTERNAL_ERROR` | Retry with the same idempotency key. |\n| `VALIDATION_FAILED` | Fix the fields in `details[]`. Retrying unchanged fails again. |\n| `PRECONDITION_FAILED` | Re-read the resource, reapply your change, send the new `ETag`. |\n| `NOT_FOUND` | The identifier does not belong to this agency. Do not retry. |\n\nReuse the idempotency key when you retry a write. That is the whole point of it:\na timeout tells you nothing about whether the reservation was created, and\nreusing the key gets you the original answer instead of a second booking.\n\n```bash\nIDEM=$(uuidgen)          # once, before the first attempt\nfor attempt in 1 2 3; do\n  code=$(curl -s -o body.json -w '%{http_code}' -X POST \"$BASE/reservations\"     -H \"Authorization: Bearer $KEY\" -H \"Idempotency-Key: $IDEM\"     -H \"Content-Type: application/json\" -d \"$PAYLOAD\")\n  case $code in\n    2*) break ;;                       # done — first attempt or replay\n    4*) [ \"$code\" = 429 ] || break ;;  # client error: stop, do not retry\n  esac\n  sleep $((attempt * 5))\ndone\n```\n\nLog `error.requestId` on every failure. It is what support needs to find the\nrequest on our side, and it is the only field that identifies your call\nuniquely.\n\n## Errors\n\nEvery failure returns the same envelope:\n\n```json\n{\n  \"error\": {\n    \"code\": \"VALIDATION_FAILED\",\n    \"message\": \"The request payload failed validation.\",\n    \"details\": [{ \"field\": \"adults\", \"issue\": \"must not be less than 1\" }],\n    \"requestId\": \"req_8f21c0\"\n  }\n}\n```\n\nBranch on `error.code` — it is part of the contract. The `message` is written\nfor a human and may be reworded at any time. `details` appears only when\nspecific fields can be named. `requestId` is always present; quote it in\nsupport requests.\n\nA resource belonging to another tenant is indistinguishable from one that does\nnot exist: both return `NOT_FOUND`. A different status would let the response\ncode confirm which identifiers are real.\n\n| Code | HTTP | Meaning | What to do |\n| --- | --- | --- | --- |\n| `UNAUTHENTICATED` | 401 | No API key was supplied. | Send an Authorization: Bearer <key> header. |\n| `INVALID_API_KEY` | 401 | The API key is unknown, expired or revoked. | Check the key, or issue a new one from the client portal. |\n| `IP_NOT_ALLOWED` | 403 | The request origin is not in the approved IP allowlist for this tenant. | Request approval for this IP address from the client portal. |\n| `FEATURE_NOT_ENABLED` | 403 | The public API add-on is not enabled for this tenant. | Contact tibs to enable the public API. |\n| `INSUFFICIENT_SCOPE` | 403 | The key lacks the scope required for this endpoint. | Issue a key that carries the required scope. |\n| `NOT_FOUND` | 404 | The resource does not exist. | Check the identifier. |\n| `VALIDATION_FAILED` | 422 | The request payload failed validation. | See details[] for the offending fields. |\n| `IDEMPOTENCY_REQUIRED` | 422 | Write requests must carry an Idempotency-Key header. | Send a unique UUID in the Idempotency-Key header and reuse it when retrying. |\n| `IDEMPOTENCY_CONFLICT` | 409 | This Idempotency-Key was already used with a different request body. | Use a new key for a different request. |\n| `IDEMPOTENCY_IN_PROGRESS` | 409 | A request with this Idempotency-Key is still being processed. | Retry in a few seconds. |\n| `PRECONDITION_REQUIRED` | 428 | This update requires an If-Match header. | Read the resource first and send back its ETag in If-Match. |\n| `PRECONDITION_FAILED` | 412 | The resource changed since the version you read. | Re-read the resource, reapply your change and retry. |\n| `RATE_LIMITED` | 429 | Too many requests for the current plan. | Retry after the number of seconds given in the Retry-After header. |\n| `INTERNAL_ERROR` | 500 | Unexpected server error. | Retry later; quote the requestId when contacting support. |\n\n## Versioning\n\nBreaking changes take a new major version in the path. Within `v1` the\nfollowing will not happen:\n\n- removing or renaming an endpoint, field or query parameter\n- changing the type or format of a field\n- adding a required request field\n- changing the meaning of an error code\n- tightening validation so a previously accepted request is refused\n\nThe following can happen at any time, and a client must tolerate them:\n\n- new endpoints\n- new optional request fields\n- **new response fields** — parse permissively; a strict schema that rejects\n  unknown keys will break on the first extension\n- new values in enumerations documented as extensible\n\nAt most two major versions run at once. A version being retired returns\n`Deprecation: true`, a `Sunset` date and a `Link` to the migration notes,\nwith a minimum six-month window. It is switched off only after the usage log\nshows who still calls it and those callers have been told.\n\n`info.version` is semver over the contract and moves independently of the\npath version.\n\n## Changelog\n\n**1.0.0** — Writes. `POST /contacts`, `PATCH /contacts/{id}`,\n`POST /reservations`, idempotency on all writes, a separate write rate limit.\n\n**0.2.0** — Reads for the catalogue and reservations, `reservations:pii` for\ntraveller names.\n\n**0.1.0** — Keys, IP allowlist, scopes and contact reads.",
    "termsOfService": "https://api.tibs.bg/terms",
    "title": "tibs ERP API",
    "version": "1.0.0"
  },
  "openapi": "3.0.0",
  "paths": {
    "/api/public/v1/contacts": {
      "get": {
        "description": "Returns the tenant's contacts, oldest identifier first. Personal identifiers are included only for keys carrying the contacts:pii scope.",
        "operationId": "listContacts",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "maximum": 100,
              "minimum": 1,
              "type": "number"
            }
          },
          {
            "description": "The id of the last item on the previous page.",
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Returns contacts changed at or after this instant.",
            "in": "query",
            "name": "updatedSince",
            "required": false,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicContactListDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Plan rate limit exceeded."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "List contacts",
        "tags": [
          "Contacts"
        ]
      },
      "post": {
        "description": "Creates a contact. Requires an Idempotency-Key; writing personal identifiers requires the contacts:pii scope and is refused rather than silently dropped without it.",
        "operationId": "createContact",
        "parameters": [
          {
            "description": "UUID; reuse the same value when retrying.",
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateContactDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicContactDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "Create a contact",
        "tags": [
          "Contacts"
        ]
      }
    },
    "/api/public/v1/contacts/{id}": {
      "get": {
        "description": "Returns one contact. A contact belonging to another tenant is reported as missing, not forbidden.",
        "operationId": "getContact",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicContactDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "No such contact for this tenant."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "Retrieve a contact",
        "tags": [
          "Contacts"
        ]
      },
      "patch": {
        "description": "Applies a partial update. Requires the ETag of the version you read in If-Match, so a concurrent change is refused instead of overwritten.",
        "operationId": "updateContact",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "ETag returned by the last read of this contact.",
            "in": "header",
            "name": "If-Match",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateContactDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicContactDto"
                }
              }
            },
            "description": ""
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "No such contact for this tenant."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "Update a contact",
        "tags": [
          "Contacts"
        ]
      }
    },
    "/api/public/v1/programs": {
      "get": {
        "description": "Returns the published catalogue with duration, route, inclusions and the entry price.",
        "operationId": "listPrograms",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "maximum": 100,
              "minimum": 1,
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "updatedSince",
            "required": false,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProgramListDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Plan rate limit exceeded."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "List programs",
        "tags": [
          "Programs"
        ]
      }
    },
    "/api/public/v1/programs/{id}": {
      "get": {
        "description": "Returns one programme with its full published detail.",
        "operationId": "getProgram",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicProgramDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "No such program for this tenant."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "Retrieve a program",
        "tags": [
          "Programs"
        ]
      }
    },
    "/api/public/v1/reservations": {
      "get": {
        "description": "Returns the tenant's reservations. Traveller names are included only for keys carrying the reservations:pii scope.",
        "operationId": "listReservations",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "maximum": 100,
              "minimum": 1,
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "updatedSince",
            "required": false,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicReservationListDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Plan rate limit exceeded."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "List reservations",
        "tags": [
          "Reservations"
        ]
      },
      "post": {
        "description": "Creates a reservation against a catalogue programme. The price is computed server-side from the departure and passenger count and is never taken from the request. The reservation is created pending and is not dispatched to the supplier.",
        "operationId": "createReservation",
        "parameters": [
          {
            "description": "UUID; reuse the same value when retrying.",
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateReservationDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicReservationDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Unknown program or contact."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "Create a reservation",
        "tags": [
          "Reservations"
        ]
      }
    },
    "/api/public/v1/reservations/{id}": {
      "get": {
        "description": "Returns one reservation. Cost price and commission are never included.",
        "operationId": "getReservation",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicReservationDto"
                }
              }
            },
            "description": ""
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "Missing or invalid API key."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "IP not allowed, feature disabled, or insufficient scope."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            },
            "description": "No such reservation for this tenant."
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ],
        "summary": "Retrieve a reservation",
        "tags": [
          "Reservations"
        ]
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "servers": [
    {
      "description": "Production",
      "url": "https://erp.tibs.bg"
    }
  ],
  "tags": [
    {
      "description": "Customer and company records held by the agency.",
      "name": "Contacts"
    },
    {
      "description": "The published catalogue: what the agency sells, with departure dates and prices.",
      "name": "Programs"
    },
    {
      "description": "Bookings against a catalogue programme. Created pending and never dispatched to the supplier — that is a separate, explicit action.",
      "name": "Reservations"
    }
  ]
}
