# Changelog

API changelog and release history.

> Enterprise API · https://developer.telgea.com/docs/enterprise/changelog

## Unreleased [#unreleased]

### 2026-10-19 [#2026-10-19]

* **Analytics**: `GET /v1/analytics` answers `400 BAD_REQUEST` to an empty `msisdn`. It used to
  ignore the parameter and report usage for every number in scope.
* **Users**: `GET /v1/users` answers `400 BAD_REQUEST` to an empty or blank `employeeId`. It used to
  ignore the parameter and list every user. If you build either query from a value that can be
  empty, leave the parameter out instead.

- **User plans**: `GET /v1/user-plans/search` answers `400 BAD_REQUEST` to an empty `iccid`.
- **User plans**: `POST /v1/user-plans/{userId}/termination` now ends a plan whose number is
  suspended. The number stays suspended until the end date and is deactivated 14 days later, as any
  ended number is. Before, the request was accepted but the number was never ended. The same holds
  for a number suspended after its end was booked. Until the end date such a plan reads
  `TERMINATING` with `terminationScheduledAt` set, where it used to read `INACTIVE`.
- **User plans**: a plan whose number is suspended now reads `ACTIVE`, where it used to read
  `TERMINATING`. Suspending a number doesn't end the plan, and the plan is still billed while the
  number is suspended. Such a plan reads `TERMINATING` only once its end is booked. If you filter
  user plans by `status`, suspended numbers now come back under `ACTIVE`.

### 2026-10-16 [#2026-10-16]

### 2026-10-15 [#2026-10-15]

* **Data pools**: the docs for `GET /v1/data-pools/{poolId}/members` now say to count `usageBytes`
  once per `userId`. A member with several lines is listed once per line, and each row carries the
  member's usage across all their lines, so summing the rows counts that member more than once. The
  response itself has not changed. If you add up `usageBytes` across rows, deduplicate by `userId`
  first.

- **Webhooks**: `UserPlanCreated` and `UserPlanActive` fire once for each local number a user
  takes, not only for their first. Both payloads add `localNumbers`, every local number the user
  holds, oldest first. `UserPlanActive` keeps `localNumber`, which still describes the user's first
  local number. For a user with one number nothing else changes: `localNumbers` holds that one
  number. Each item's `sim` is `null` while the number has no eSIM yet.

### 2026-10-12 [#2026-10-12]

* **All endpoints**: every name you send or read is camelCase. This renames the snake\_case
  names that were left. On `GET /v1/usage`, the query is `startDate`, `endDate`, `pageSize` and
  `userId`. Each record has `timeStarted`, `timeEnded`, `servingCountryCode`, `usageClass`,
  `counterpartyMsisdn`, `callDirection`, `counterpartyCountryCode`, `networkType`, `userId`,
  `totalPrice`, `unitPrice` and `rateId`, and a page has `nextCursor` and `hasMore`. On
  `GET /v1/analytics`, the query is `startDate`, `endDate`, `userId`, `countryIsoCode` and
  `orderBy`. On `GET /v1/rates`, it is `servingCountry` and `asOfDate`, and each rate has
  `ratePerUnit` and `validFrom`. Every error carries `requestId`, and its extension members are
  camelCase too: `missingPermission`, `revokedAt`, `countryCode`, `planEndedAt`, `currentState`,
  `requiredStates`, `executionDate`, `firstDate` and `lastDate`. The old names are no longer
  accepted or returned.
* **All endpoints**: since 2026-10-06, every error is an
  [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457) body served as
  `application/problem+json`. It's a flat object with `type`, `title`, `status`, `detail`, `code`
  and `request_id`, where it used to be wrapped in `error`. `message` is now `detail`, and `hint`
  is gone: `type` is a URI that opens the error's own page, which says what to do about it. `code`
  keeps its values, and `fields` and `missing_permission` keep their place, now at the top level.
  Many refusals that used to answer with a generic code have their own `type`, such as
  `hub/PORT_IN_ALREADY_IN_PROGRESS`. Branch on `code` or `type`. See
  [Errors](https://developer.telgea.com/docs/enterprise/guides#error-handling).
* **User plans and port-ins**: a market can require your organization to pass business verification
  before it provisions a user plan or ports a number in there. Until it has, `POST /v1/user-plans`
  and `POST /v1/port-ins` answer `403 COUNTRY_NOT_SUPPORTED` with the type
  `ORGANIZATION_VERIFICATION_REQUIRED`, and nothing is created. Send the request again once your
  organization has passed. No market requires verification yet.
* **Port-ins**: since 2026-10-05, `POST /v1/port-ins/validate` needs the `port-ins:validate`
  permission instead of `port-ins:create`. Every key that held `port-ins:create` was given it, so
  no existing key needs a change. Grant it in Console to a new key that validates port-ins.
* **Port-ins**: the execution date must be one of the dates the market offers, and the 7-day minimum
  is gone. Each type on `GET /v1/port-ins/fields/{countryCode}` lists the dates in
  `executionDate.dates`, which hold until the market's midnight, `executionDate.validUntil`.
  `GET /v1/port-ins/fields/{countryCode}/schema` carries the same list as `$defs.executionDate`. How
  soon the first date is and how far ahead the last differ by market, and weekends are refused in
  every market. See the [port-in guide](https://developer.telgea.com/docs/enterprise/guides/port-in#step-1-discover-required-fields).
* **Port-ins**: `POST /v1/port-ins/validate` and `POST /v1/port-ins` refuse a date outside the list
  with `400 BAD_REQUEST` and the type `provider-platform/EXECUTION_DATE_NOT_OFFERED`, which names the
  market's first and last dates. The date is checked once every other field passes. Until 2026-10-12
  only some markets checked the date, and a refused one answered `200` with `valid: false` or
  `status: "REJECTED"`. A malformed `executionDate` answers `VALIDATION_ERROR`, and
  `earliest_execution_date` is gone with the 7-day minimum.
* **Port-ins**: `GET /v1/port-ins/fields/{countryCode}` has carried texts for guiding your customer
  through the form since 2026-10-07. They were not announced until now. Each type can carry
  `explanation`, to help pick it where a market has more than one, and `timing`: when the port-in
  is submitted to the carrier, who sets the final date, whether it can still move, and up to when
  it can be cancelled or rescheduled. Each field can carry `help` (what the value is and whose
  details it is), `howToGet` (where to find a value such as a PAC) and `example` (a value in the
  accepted format). Each text has an `id` that never changes, to key a translation off, and the
  English in `en`. See the [port-in guide](https://developer.telgea.com/docs/enterprise/guides/port-in#step-1-discover-required-fields).
* **Port-ins**: each type on `GET /v1/port-ins/fields/{countryCode}` carries `typeLabel` in a
  market with more than one type: the label over the choice of type, such as "Who is the number
  registered to?", to head the choice beside each type's `explanation`.
* **Port-ins**: since 2026-10-07, each entry in `errors` from `POST /v1/port-ins/validate`, and
  from a `REJECTED` answer to `POST /v1/port-ins`, names its `field` and carries a `code` to branch
  on: `REQUIRED`, `FORMAT_INVALID`, `LENGTH_INVALID`, `NOT_ALLOWED` or `DATE_NOT_ALLOWED`. Since
  2026-10-08 it can also be `CHECK_DIGIT_INVALID` or `WRONG_CUSTOMER_TYPE`. New codes will be
  added; show the `message` for one you don't recognise. The
  [port-in guide](https://developer.telgea.com/docs/enterprise/guides/port-in#step-2-validate-before-submission) says what each means.
* **Port-ins**: since 2026-10-08, validating or creating a port-in catches typos the carrier would
  otherwise refuse days later. A wrong check digit in a Swedish personnummer or
  organisationsnummer, a Spanish DNI or NIE, or a French RIO answers `CHECK_DIGIT_INVALID`. A
  Swedish organisationsnummer sent with `personal` answers `WRONG_CUSTOMER_TYPE`. A French landline
  RIO and a German date of birth in the future answer `NOT_ALLOWED`. Values are also accepted as
  people type them: spaces, separators and lower case in a UK PAC, a French RIO, Spanish and
  Swedish ID numbers and SIM numbers, and a US ZIP+4. A SIM number in Denmark and Belgium no longer
  needs at least 18 digits.
* **Port-ins**: since 2026-10-05, `POST /v1/port-ins/validate` and `POST /v1/port-ins` answer
  `409 CONFLICT` for a number Telgea already holds: one on a Telgea line, or one whose plan ended in
  the last 14 days and can still be restored. Validating a number with a port-in already in
  progress answers `409` too, as creating one always has. Neither creates anything.

### 2026-10-09 [#2026-10-09]

* **Port-ins**: `POST /v1/port-ins/{portInId}/discard` discards a port-in that ended in `REJECTED` or
  `FAILED`. The number leaves the plan, which reads `INACTIVE` unless the user has another number,
  so you can give the user a new number or port in a different one. A port-in that can't be
  discarded answers `400 BAD_REQUEST` with the type `PORT_IN_NOT_DISCARDABLE`. It needs the new
  `port-ins:discard` permission, which no existing key holds; add it to your key in Console.

### 2026-10-08 [#2026-10-08]

* **Port-ins**: `failure` on `GET /v1/port-ins/{portInId}` and `GET /v1/port-ins/number/{numberId}`
  carries `text`: what happened and what to do next, in English, for showing to your customer. It
  includes the market's advice where there is some, such as where to find a SIM number, and is set
  even when `reason` is `null`. Its wording may change at any time; keep branching on `reason` and
  `field`.

- **Webhooks**: `failureDetails` on `PortInRejected` and `PortInFailed` carries `text` too, written
  when the event is sent. It can leave out market advice that `GET /v1/port-ins/{portInId}`
  includes.

### 2026-10-01 [#2026-10-01]

* **Usage**: `GET /v1/usage` returns records in the order we stored or last corrected them, so a
  partner that resends the cursor of its last page receives late records and corrections it has not
  seen. Expect usage from the last 24–48 hours to keep changing as late records arrive and are rated.
* **All endpoints**: the per-key request quota now resets 24 hours after the window opens, as
  documented. It previously only reset after a key had been completely idle for 24 hours, so a key
  called more often than once a day spent its 1000 requests over its lifetime and was then refused
  with `RATE_LIMIT_EXCEEDED` until it went quiet for a full day. A key already in that state starts a
  fresh window on its next request; no change is needed on your side.

### 2026-09-23 [#2026-09-23]

* **User plans**: the provisioning guide notes that in a market whose operator publishes no API a
  change can take a couple of hours rather than minutes. No endpoint behaves differently — poll, and
  do not read a long `PROVISIONING` as a failure.

### 2026-09-21 [#2026-09-21]

* **Usage**: the usage guide gives the typical delay before each market's usage reaches the feed,
  and says to re-read from the previous day on every poll. The feed was described as near real time;
  in Belgium and Spain a record typically arrives 13 to 16 hours after the usage.
* **User plans**: the provisioning guide gives the average time to provision a line in each market,
  from under a minute to 35 minutes in the United States.
* **Port-ins**: an execution date in Spain, Sweden or the United Kingdom must be a weekday.
  This was already enforced, but not documented.

### 2026-09-18 [#2026-09-18]

* **Documentation**: the reference is split into the Enterprise API and the Partner API, with one
  page per resource and each operation naming the permission its key needs. Links to the old
  one-page-per-operation reference redirect to that operation on its resource page.
* **All endpoints**: the `Idempotency-Key` header is now declared on every endpoint that changes
  data, so it appears in the reference, the playground and the OpenAPI description. Its behaviour is
  unchanged since 2026-09-16.
* **User plans**: `DELETE /v1/user-plans/{userId}/data-restriction` lists each restored number once.
  A customer whose line draws on several data pools used to see the number repeated once per pool.
* **Errors**: stopping and restoring data now say what went wrong instead of answering with a
  generic message: data already stopped (`409`), nothing to restore (`409`), no line in one of your
  data pools (`422`), and a number that cannot be stopped yet (`400`). Each hint names the call that
  fixes it. The usage feed says the same of a cursor it did not issue (`400`).
* **Usage**: a window over 31 days, or ending before it starts, is refused as a `VALIDATION_ERROR`
  naming `end_date`, instead of a generic `400`.
* **Errors**: replacing a SIM inside the cooldown now says the number's current SIM is too new to
  replace. The cooldown counts from when that SIM was issued, so it also applies to a line
  provisioned moments ago.
* **Reference**: every endpoint documents the `429` returned once an API key has used its request
  quota. The quota always applied to every endpoint; only three documented it.

### 2026-09-17 [#2026-09-17]

* **User plans**: `POST /v1/user-plans/{userId}/termination` ends a customer's line on a date you
  name. The line is suspended on that date and deactivated at the network 14 days later, so a `202`
  means the termination is booked rather than already carried out. Read it back on
  `GET /v1/user-plans/{userId}`, which now carries `terminationScheduledAt` — the date the plan is
  scheduled to end, or `null` when none is. Billing is not prorated: the customer keeps the period
  they already bought, and `activeUntil` in the response says when it ends. Send `deleteUser: true`
  to delete the end-user with the line; otherwise `DELETE /v1/users/{userId}` succeeds once the line
  is suspended, without waiting out the 14 days. The endpoint requires the `orders:cancel` permission
  on your key.

### 2026-09-16 [#2026-09-16]

* **Port-ins**: a port-in that ended in `REJECTED` or `FAILED` now carries a `failure` object on
  `GET /v1/port-ins/{portInId}` and `GET /v1/port-ins/number/{numberId}`, giving the reason it ended
  that way and the field the donor objected to, where one was identified. `failure.reason` is `null`
  when the cause could not be classified; treat an unrecognised or absent reason as an unexplained
  failure rather than an error. New reasons will be added over time.
* **Port-ins**: submitting a port-in for a number that already has one in flight now answers
  `409 CONFLICT` with a message naming the conflict, instead of a generic error. This was always the
  behaviour; it is now a documented response. Retry once the previous attempt has reached a terminal
  state: where `failure.reason` and `failure.field` identify a correctable input, fix it before
  resubmitting; where the reason is `null` or one you do not recognise, resubmit as it stands.
* **User plans**: `POST /v1/user-plans/{userId}/data-restriction` stops a customer's mobile data
  while leaving calls and texts working — for someone who has used up what they paid for — and
  `DELETE` on the same path restores it. Both take an optional `reason`, kept with the change and
  visible to our support staff. They require `numbers:suspend` and `numbers:unsuspend` on your key.
* **User plans**: `POST /v1/user-plans/{userId}/suspension` stops all traffic on a number, domestic
  and international — for a phone that has been lost or stolen — and `DELETE` restores it. Name the
  number with `numberId`, which you get from `GET /v1/user-plans/{userId}`. The response reports the
  numbers the change actually reached at the network. Unlike the data pair these carry no `reason`:
  a suspension is a state of the line at the provider with nowhere to record one. Same permissions
  as above.
* **All endpoints**: requests that change state (`POST`, `PATCH`, `DELETE`) now accept an
  optional `Idempotency-Key` header. Retrying a request with the same key returns the original
  response instead of creating a duplicate or failing with a conflict, so a dropped connection no
  longer needs special handling. A key identifies one request to one endpoint and is remembered for
  24 hours, replaying the original outcome including a failure — use a new key once you change the
  request. Keys are private to your API key and organisation. The header is ignored on `GET`, and a
  value outside 1–255 printable ASCII characters is rejected with `400 BAD_REQUEST`. Requests that
  send no key are unaffected.

### 2026-09-15 [#2026-09-15]

* **Data pools**: `GET /v1/data-pools` lists every active pool in your organization with how much of
  each is left — bought, consumed and remaining, in GB — and whether any number drawing on it is
  currently restricted. `GET /v1/data-pools/{poolId}/members` lists the users drawing on one pool
  with the bytes each has taken, so you can see which customer to throttle before the pool runs out.
  A pool belonging to another organization answers `404`, not `403`. Both require the `pools:read`
  permission on your key.
* **Rates**: New `GET /v1/rates` returns the pay-as-you-go retail price list for a market — international
  calling, SMS and MMS per destination, or roaming data per visited country. Pass the home-plan `market`
  (ISO 3166-1 alpha-2) and a rate `type`; each rate carries the country, the per-unit price as a number
  to six decimal places, its unit, the currency and when it took effect. Prices are your organization's,
  with any negotiated rate in place of the list price. Calling and messaging are priced from the market by default —
  add `serving_country` to price that plan while roaming (`market=DE&serving_country=DK`), and the
  response echoes the `serving_country` it used. `ROAMING_DATA` prices every visited country at once, so
  it ignores the parameter and answers with `serving_country: null`. Calls and messages are priced
  outbound by default; add `direction=inbound` to price being reached instead — chargeable while
  roaming — and the response echoes the `direction` it used (`null` for `ROAMING_DATA`). Prices are those in
  effect today; add `as_of_date` (YYYY-MM-DD) to read another day's list, echoed as `as_of_date`. Every country is listed: one that
  costs nothing carries `rate_per_unit: 0` rather than being left out — whether it is priced free
  (calls inside the EU/EEA) or has no published pay-as-you-go price, such as a roaming-data country
  included in the plan. A market no carrier serves answers `403 COUNTRY_NOT_SUPPORTED`. Requires the
  `rates:read` scope.

### 2026-09-14 [#2026-09-14]

* **Usage**: `GET /v1/usage` returns raw voice, SMS and data records as one chronological feed, so
  you do not have to know that we store calls and data separately. Page with `cursor` and
  `next_cursor` rather than page numbers — a cursor is stable while new records arrive behind it.
  `page_size` is at most 100, the `start_date`/`end_date` window at most 31 days, and a key may make
  1000 requests per 24 hours. Prices are `null` until a record is rated and on usage covered by a
  pool. Requires the `consumption:read` permission on your key.
* **Analytics**: `GET /v1/analytics` returns the same consumption already totalled per `interval`
  (`day` or `month`), with the highest, lowest and average interval across the window, so a dashboard
  does not have to add up the raw feed. Narrow it with `user_id`, `msisdn` or `country_iso_code`.
  Requires the `analytics:read` permission on your key.

### 2026-09-11 [#2026-09-11]

* **Users**: `firstName` and `lastName` must now be non-blank when you create a user, and when you
  include them while updating one. A blank or whitespace-only name is rejected with a
  `400 VALIDATION_ERROR` that names the field; leading and trailing whitespace on names is trimmed
  before the user is stored. Requests that already send real names are unaffected.

## v1.0.0 [#v100]

Initial public API release.

* **Users**: List, create, update, and delete organization users
* **Teams**: List and create teams for user grouping
* **User Plans**: List, get, and create telecommunications plans
