# Transfer Number to Telgea

Transfer existing phone numbers from other carriers to Telgea.

> Enterprise API · https://developer.telgea.com/docs/enterprise/guides/port-in

## Number Porting [#number-porting]

Number porting lets users transfer their existing phone numbers from another carrier to Telgea. The process varies by country — different markets require different authentication methods, personal details, and carrier-specific information.

This guide covers the full porting lifecycle: discovering field requirements, validating data, submitting a port-in, scheduling, and tracking progress through to completion.

## Lifecycle [#lifecycle]

Who does what, in order. The dotted loop is the resubmission after a rejection.

Every port-in follows this state progression. The full explanation, with what to do in each state, is on the [port-in lifecycle](https://developer.telgea.com/docs/enterprise/concepts/port-in-lifecycle) page.

A port-in can be rejected at two points: during the initial submission to the provider, or later during the acceptance check phase. Similarly, failures can occur at multiple stages — during submission, acceptance checking, or the final execution workflow. In all cases, the port-in's `failure` describes what went wrong.

## Step 1: Discover Required Fields [#step-1-discover-required-fields]

Steps 1 and 2 are safe to try in the [playground](https://developer.telgea.com/playground#tag/port-ins) first — reading the fields and validating your data change nothing. Step 3 does: it starts a real port-in with the customer's current carrier.

Each country has different requirements for porting. Use the fields endpoint to discover what information you need to collect:

```bash
GET /v1/port-ins/fields/{countryCode}
```

The response is keyed by port-in type (e.g. `personal` and `business`, or `default` in a market with one type). Each type contains a human-readable label, an array of field definitions with validation rules, and the transfer date as `executionDate`. Field types include `text`, `select`, `date`, `number`, and `checkbox`. The United Kingdom's one type takes a PAC:

```json op="GET /v1/port-ins/fields/{countryCode}" dir=response status=200
{
  "default": {
    "label": "Default",
    "fields": [
      {
        "name": "authorizationCode",
        "label": "PAC code",
        "type": "text",
        "description": "Porting Authorization Code from the current carrier. The code is valid for 30 days.",
        "errorMessage": "A PAC is 3 letters, then 6 digits (e.g. ABC123456). 6 digits then 3 letters is a STAC, not a PAC.",
        "howToGet": {
          "id": "port-in.gb.authorizationCode.howToGet",
          "en": "If the number is in a person's name, text PAC to 65075 from the phone with this number. If it's in a company's name, the company asks the current carrier for the PAC."
        },
        "example": "ABC123456",
        "regex": "/^\\s*[A-Za-z]{3}\\s*\\d{3}\\s*\\d{3}\\s*$/"
      }
    ],
    "executionDate": {
      "name": "executionDate",
      "label": "Transfer date",
      "type": "date",
      "errorMessage": "That date isn't possible in the UK.",
      "help": {
        "id": "port-in.executionDate.help.a-weekday-from-the-next-working-day-to-30-days-from-today",
        "en": "A weekday, from the next working day to 30 days from today."
      },
      "minDate": "2027-03-01",
      "maxDate": "2027-03-29",
      "dates": [
        "2027-03-01", "2027-03-02", "2027-03-03", "2027-03-04", "2027-03-05",
        "2027-03-08", "2027-03-09", "2027-03-10", "2027-03-11", "2027-03-12",
        "2027-03-15", "2027-03-16", "2027-03-17", "2027-03-18", "2027-03-19",
        "2027-03-22", "2027-03-23", "2027-03-24", "2027-03-25", "2027-03-26",
        "2027-03-29"
      ],
      "validUntil": "2027-02-27T00:00:00.000Z"
    },
    "timing": {
      "submitted": "immediately",
      "finalDateSetBy": "us",
      "dateMayMove": true,
      "cancel": "never",
      "reschedule": "never"
    }
  }
}
```

`executionDate.dates` lists every date the port-in can be executed on, as of the day you asked: send one of them in Step 2 and Step 3. The list holds until `validUntil`, the market's next midnight, when its first date moves on. `minDate` and `maxDate` are its first and last dates, for a date picker's bounds, and `help` describes the range in words you can show your customer.

The texts help you guide your customer through the form. Each is an object with an `id` that never changes, to key a translation off, and the English text in `en`. A type or field that has nothing to say leaves the text out:

* `typeLabel` on a type heads the choice of type, such as "Who is the number registered to?", in a market with more than one type. It is the same in every type.
* `explanation` on a type helps your customer pick it, in a market with more than one type.
* `help` on a field says what the value is and whose details it is.
* `howToGet` on a field says where to find a value your customer has to look up, such as the PAC above.

A field can also carry `example`, a plain string in the format the field accepts, such as `ABC123456` above. It shows the format only and is never a real value.

`timing` says how a port-in runs in the market: when it is `submitted` to the carrier, who sets the final transfer date (`finalDateSetBy`), whether the date can still move once submitted (`dateMayMove`), and up to when the port-in can be cancelled (`cancel`) or its date changed (`reschedule`). It is the same in every type of a market.

**Do not hardcode field requirements.** They vary by country and provider, and can change. Always fetch them dynamically. See the [Get port-in fields](https://developer.telgea.com/docs/enterprise/api/port-ins#get-port-in-fields-for-a-country) API reference for a full example response with multiple field types. A [JSON Schema version](https://developer.telgea.com/docs/enterprise/api/port-ins#get-port-in-fields-json-schema) is also available for client-side validation, with the same dates under `$defs.executionDate`.

## Step 2: Validate Before Submission [#step-2-validate-before-submission]

Use the validate endpoint to dry-run your port-in data against provider requirements before committing:

```bash
POST /v1/port-ins/validate
```

```json op="POST /v1/port-ins/validate" dir=request
{
  "countryCode": "GB",
  "type": "default",
  "msisdn": "+447400123456",
  "executionDate": "2027-03-01",
  "fields": {
    "authorizationCode": "ABC123456"
  }
}
```

This is a read-only operation — no resources are created or modified. The response indicates whether the data would pass:

```json op="POST /v1/port-ins/validate" dir=response status=200
{
  "valid": true,
  "errors": []
}
```

If validation fails, `valid` is `false` and `errors` holds one entry per refused value. Each names its `field` (a key you sent in `fields`, or `type`), a `code` to branch on and a `message` in English. A PAC typed the wrong way round is a STAC:

```json op="POST /v1/port-ins/validate" dir=response status=200
{
  "valid": false,
  "errors": [
    {
      "field": "authorizationCode",
      "code": "FORMAT_INVALID",
      "message": "A PAC is 3 letters, then 6 digits (e.g. ABC123456). 6 digits then 3 letters is a STAC, not a PAC."
    }
  ]
}
```

| Code                  | The value                                                                                                                                                                                                                 |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REQUIRED`            | is missing or empty                                                                                                                                                                                                       |
| `FORMAT_INVALID`      | isn't in the field's format, such as a PAC that isn't 3 letters then 6 digits                                                                                                                                             |
| `LENGTH_INVALID`      | is shorter than the field's `minLength` or longer than its `maxLength`                                                                                                                                                    |
| `NOT_ALLOWED`         | is well formed but not one the market accepts: a value outside `options` or outside `min` and `max`, a date of birth in the future, or a French landline RIO                                                              |
| `CHECK_DIGIT_INVALID` | has a check digit, check letter or key that doesn't match the rest of it: a typo, or a code that belongs to another number. Checked for Swedish personnummer and organisationsnummer, Spanish DNI and NIE, and French RIO |
| `WRONG_CUSTOMER_TYPE` | belongs under another of the market's types, such as a Swedish organisationsnummer sent with `personal`. Send the other type                                                                                              |
| `DATE_NOT_ALLOWED`    | is a date the field doesn't accept. A transfer date outside `executionDate.dates` is refused with a `400` instead; see [Constraints](#constraints)                                                                        |

A code is never renamed or removed, but new ones will be added. Show the `message` for a code you don't recognise. Submitting the port-in without validating it first returns the same `errors`, as described in [Step 3](#response).

The phone number is checked before any of this. An `msisdn` that isn't a valid phone number, or that belongs to another country than `countryCode`, is refused with `400 VALIDATION_ERROR` and named in its `fields` (see [Validation errors](https://developer.telgea.com/docs/enterprise/guides#validation-errors)), both here and on submission. It never appears in `errors`.

Always validate before submitting to catch problems without side effects.

A number that cannot be ported in at all is refused with `409 CONFLICT`, exactly as submitting it would be: one Telgea already holds, or one with a port-in already in progress.

See the [Validate port-in data](https://developer.telgea.com/docs/enterprise/api/port-ins#validate-port-in-data) API reference.

## Step 3: Submit the Port-In [#step-3-submit-the-port-in]

<Callout type="warn" title="This one is real">
  Submitting a port-in — from your code or from the playground — asks the customer's current carrier to move the number. There is no sandbox to rehearse it in.
</Callout>

Once validation passes, submit the port-in:

```bash
POST /v1/port-ins
```

```json op="POST /v1/port-ins" dir=request
{
  "userId": "123e4567-e89b-12d3-a456-426614174000",
  "countryCode": "GB",
  "msisdn": "+447400123456",
  "executionDate": "2027-03-01",
  "type": "default",
  "fields": {
    "authorizationCode": "ABC123456"
  }
}
```

### Constraints [#constraints]

* **Execution date**: Must be one of the type's `executionDate.dates` from [Step 1](#step-1-discover-required-fields). A date outside the list is refused with `400 BAD_REQUEST` and the type `provider-platform/EXECUTION_DATE_NOT_OFFERED`, which names the first and last dates the market offers, both here and on the validate endpoint. The date is checked once every other field passes, so field errors still come back first, as `valid: false` or `status: "REJECTED"`. A date that isn't `YYYY-MM-DD` is refused with `400 VALIDATION_ERROR`
* **One active port-in per number**: If a port-in is already in progress (not completed, failed, or rejected) for the same MSISDN, the request will be rejected
* **Not already with Telgea**: A number on a Telgea line, or one whose line ended within the last 14 days and can still be restored, is refused with `409 CONFLICT`. To get back a number whose line you ended, contact support within those 14 days
* **Plan state**: The user must hold no current phone number, or have a previously unsuccessful port-in

### Response [#response]

The response confirms the port-in was created with `status: "CREATED"`. The actual submission to the losing carrier happens asynchronously — track its progress as described in Step 4.

```json op="POST /v1/port-ins" dir=response status=200
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "CREATED",
  "msisdn": "+447400123456",
  "numberId": "123e4567-e89b-12d3-a456-426614174000",
  "executionDate": "2027-03-01"
}
```

If you skip the validate endpoint and the data fails pre-flight validation, the response will have `status: "REJECTED"` with an `errors` array. Using the [validate endpoint](#step-2-validate-before-submission) first avoids this.

See the [Submit a port-in request](https://developer.telgea.com/docs/enterprise/api/port-ins#submit-a-port-in-request) API reference.

## Step 4: Track Progress [#step-4-track-progress]

### Polling [#polling]

Retrieve the current status of a port-in at any time:

```bash
GET /v1/port-ins/{portInId}
```

```json op="GET /v1/port-ins/{portInId}" dir=response status=200
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "countryCode": "GB",
  "msisdn": "+447400123456",
  "executionDate": "2027-03-01",
  "type": "default",
  "status": "SUBMITTED",
  "numberId": "123e4567-e89b-12d3-a456-426614174000",
  "createdAt": "2027-02-26T10:00:00Z",
  "updatedAt": "2027-02-26T10:05:00Z",
  "failure": null
}
```

To see the full porting history for a number (including past port-ins):

```bash
GET /v1/port-ins/number/{numberId}?page=1&pageSize=50
```

See the [Get port-in status](https://developer.telgea.com/docs/enterprise/api/port-ins#get-port-in-status) and [List port-ins for a number](https://developer.telgea.com/docs/enterprise/api/port-ins#list-port-ins-for-a-number) API references.

### Webhooks [#webhooks]

Subscribe to webhook events for real-time notifications as the port-in progresses. See the [Webhooks guide](https://developer.telgea.com/docs/enterprise/guides/webhooks) for setup and security details.

| Event             | Fired when                                                                                      |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| `PortInSubmitted` | Request sent to the losing carrier                                                              |
| `PortInAccepted`  | Losing carrier accepts; execution date confirmed                                                |
| `PortInRejected`  | Rejected by the losing carrier or by provider validation, possibly before any `PortInSubmitted` |
| `PortInSucceeded` | Number ported successfully                                                                      |
| `PortInFailed`    | Failed at any stage, or cancelled                                                               |

All port-in webhook payloads share this structure in the `data` field:

```json event=PortInAccepted
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "eventType": "PortInAccepted",
  "timestamp": "2026-03-25T14:30:00.000Z",
  "organizationId": "b5e2d7f3-a1c4-4e89-9f6b-3d8a7c5e1024",
  "data": {
    "portInId": "d4e5f6a7-b8c9-0123-d456-789012345678",
    "numberId": "f7a8b9c0-d1e2-3456-f789-012345678901",
    "phoneNumber": "+4512345678",
    "countryCode": "DK",
    "state": "ACCEPTED",
    "executionDate": "2026-04-01",
    "failureDetails": null
  }
}
```

## Scheduling Considerations [#scheduling-considerations]

The execution date is when the number will actually transfer to Telgea. Keep in mind:

* The execution date must be one of the dates the fields endpoint lists for the type. They are weekdays, and how soon the first one is and how far ahead the last one is differ by market
* The list changes at the market's midnight, `executionDate.validUntil`. If your customer leaves a form open past it, fetch the fields again before you submit, or the date they picked may have left the list
* The losing carrier confirms or adjusts the execution date during the acceptance phase — the confirmed date is on the port-in once it reaches `ACCEPTED`. Please note that the execution date submitted in the initial port-in request is the **desired** execution date — the *actual* execution date is determined by the network, and can change. Always note down the execution date once the port-in reaches `ACCEPTED`
* Build buffer time into your scheduling to account for potential rejections and resubmissions
* In a [market whose operator publishes no API](https://developer.telgea.com/docs/enterprise/concepts/markets), each step
  is confirmed in hours rather than the moment the losing carrier answers
* If a port-in is rejected, you can correct the information and submit a new port-in for the same number once the previous one is in a terminal state (`REJECTED` or `FAILED`)

## Handling Rejections and Failures [#handling-rejections-and-failures]

`GET /v1/port-ins/{portInId}` and `GET /v1/port-ins/number/{numberId}` carry a `failure` object on any port-in that ended in `REJECTED` or `FAILED`, and `null` on every other status:

```json op="GET /v1/port-ins/{portInId}" dir=response status=200 partial
{
  "status": "REJECTED",
  "failure": {
    "reason": "AUTHORIZATION_CODE_INVALID",
    "field": "authorizationCode",
    "text": "Your authorization code was refused or has expired."
  }
}
```

* **Branch on `reason`.** It can be `null`: some failures cannot yet be classified, and we would rather say so than publish a reason that is wrong. Treat `null`, or a value you do not recognise, as "this port-in failed and we cannot tell you why" and fall back on `status`. New reasons will be added over time, so handle unknown values rather than rejecting them.
* **`field`**, when present, identifies the port-in input the carrier objected to (e.g. `authorizationCode`, `ownerFirstName`, `currentCarrier`, `type`, `msisdn`).
* **Show `text` to your customer.** It says what happened and what to do next, in English, with the market's own advice where there is some — where to find a SIM number, which dates are possible. It is always set, including when `reason` is `null`. Its wording may change at any time, so never branch on it.

### Failure Reasons [#failure-reasons]

| Reason                       | Typically seen for                                                                                                                 |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `WRONG_CUSTOMER_TYPE`        | Customer type (private/business) doesn't match donor records                                                                       |
| `CONTRACT_LOCKED`            | A binding/contract at the donor blocks porting                                                                                     |
| `NUMBER_OWNERSHIP_MISMATCH`  | Donor reports the number is not owned by the stated subscriber                                                                     |
| `NUMBER_ALREADY_PORTING`     | Another active port-in already exists for this MSISDN/SIM                                                                          |
| `AUTHORIZATION_CODE_INVALID` | Authorization code (PAC/STAC/RIO/DOB/etc.) is invalid or expired                                                                   |
| `PORTING_DATE_UNAVAILABLE`   | Requested porting date isn't available at the donor                                                                                |
| `FIELD_MISSING`              | A required field was not supplied or was empty                                                                                     |
| `FIELD_INVALID`              | A field value was present but malformed or not accepted                                                                            |
| `FIELD_MISMATCH`             | A well-formed field value doesn't match what the donor has on file for this number, e.g. the SIM number (ICCID) of a different SIM |
| `REJECTED_BY_DONOR`          | Donor rejected the port-in without a more specific reason                                                                          |
| `CANCELLED`                  | The port-in was called off at the donor. There is no API for cancelling one yourself                                               |

### In webhooks [#in-webhooks]

The port-in webhooks carry the failure as `failureDetails`: a `code`, a `text` and — when we can identify them — a fine-grained `reason` and the offending `field`:

```json schema=PortInEventData partial
{
  "failureDetails": {
    "code": "INCORRECT_DETAILS",
    "reason": "AUTHORIZATION_CODE_INVALID",
    "field": "authorizationCode",
    "text": "Your authorization code was refused or has expired."
  }
}
```

The webhook's `text` is written when the event is sent and can leave out market advice that `GET /v1/port-ins/{portInId}` includes.

### Failure Codes [#failure-codes]

| Code                | Description                                                      | Suggested Action                                                |
| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- |
| `INCORRECT_DETAILS` | Submitted information doesn't match the losing carrier's records | Ask the user to verify and correct their details, then resubmit |
| `INTERNAL_ERROR`    | Unexpected error during processing                               | Retry the port-in; contact support if it persists               |
| `CANCELLED`         | Port-in was cancelled                                            | Resubmit if still needed                                        |
| `FORBIDDEN`         | Number is not eligible for porting                               | Number cannot be ported — contact support for details           |

`failure` on the port-in carries no `code`: branch on `reason` there. The coarse `code` is kept on the webhook for
compatibility, but it is being retired — building on it would leave you with work to do later.

Once a port-in reaches a terminal state — `REJECTED` or `FAILED` — a new port-in can be submitted for the same number.

### Discarding a failed port-in [#discarding-a-failed-port-in]

If you won't submit the number again, discard the port-in. The number leaves the plan it was arriving on and stays with the losing carrier, so you can obtain a new line for the user or port in a different number:

```bash
POST /v1/port-ins/{portInId}/discard
```

It answers `204` with no body. Only a `REJECTED` or `FAILED` port-in can be discarded, and not while a newer port-in for the same number is in flight or once one has succeeded; either is refused with `400`. Discarding a port-in that is already discarded succeeds and changes nothing.

The port-in stays readable at its own id and keeps its `REJECTED` or `FAILED` status. A discard shows as the number leaving the plan it was arriving on, and the number's port-ins also leave `GET /v1/port-ins/number/{numberId}`. Discarding needs the `port-ins:discard` permission on your key.

See the [Discard a failed port-in](https://developer.telgea.com/docs/enterprise/api/port-ins#discard-a-failed-port-in) API reference.
