> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# أول port-in لك: دليل التنفيذ الصحيح (LOA وPIN وتتبع FOC)

> قائمة تحقق للمرة الأولى عبر دورة حياة الرقم كاملة: مستندات ما قبل الاستيراد وPIN الحساب، وخطوات المعالج من الطلب إلى LOA موقّع، وتتبع تاريخ FOC عند التحويل، وأحداث webhook التي ينبغي الاشتراك فيها، وأسباب الرفض الخمسة التي تعطّل عمليات النقل الأولى — مع حل كل منها.

# دليل أول عملية port-in

دليلا التشغيل — [نقل رقم من الطرف إلى الطرف](https://docs.orbit.devotel.io/guides/port-numbers.md) و[الخروج الآمن برقم](https://docs.orbit.devotel.io/guides/port-out-numbers.md) — يغطيان كل نقطة نهاية وكل حالة حدية. هذه الصفحة هي قائمة التحقق الأقصر للمرة الأولى التي تنقل فيها رقمًا: ترتب القرارات، وتسمّي الشيء الواحد الذي يحميه كل خطوة، وتربط عمليات النقل المتعثرة بسبب الرفض الذي تسبب فيها. اقرأها مرة واحدة ثم نفّذ عمليتك والصفحتان مفتوحتان.

القراءات (`check` و`get` و`timeline`) تحتاج نطاق `numbers:read`; والكتابات (إنشاء، رفع LOA/توقيع/إرسال، إلغاء، ضبط PIN) تحتاج `numbers:write` ودور `owner` أو `admin` أو `developer`.

لا تضيف هذه الصفحة شيئا إلى الدليلين — إنها الخريطة بينهما لا البديل عنهما. ويبقيان هما المرجع الرسمي.

## 1. آلة الحالات التي تتنقل فيها

اتجاه نقل الرقم كلاهما آلة حالات أحادية الاتجاه. معرفة الحالة التي أنت فيها تخبرك ما الأفعال التي ما تزال مفتوحة — معظم أخطاء النقل الأول هي فعل حاولوه متأخرًا بدرجة واحدة.

### Port-in (الرقم ينتقل إلى Orbit)

`submitted → reviewing → approved → completed`، مع `rejected` و`cancelled` كفرعين نهائيين، و`supplement_submitted` كحالة انتظار بينما يزن المشغل تعديلك:

* **submitted** — الطلب موجود. قابل للإلغاء (`DELETE /numbers/porting/:id`). علامة `staging: "draft"` في استجابة الإنشاء تعني أن الصف محفوظ لكن لم يُرسل شيء إلى المشغل إطلاقًا — لا تتحرك المسودة إلا بعد رفع LOA وتوقيعه.
* **reviewing** — المشغل الخاسر يحتفظ بطلبك مع LOA الموقع ويفحص بيانات CSR. ما يزال قابلًا للإلغاء. ما بعد ذلك ليس كذلك.
* **approved** — قبِل المشغل الخاسر وأصدر (عادة) تاريخ FOC. الإلغاء الآن يعيد `409`; وستكتمل عملية النقل.
* **completed** — الرقم حي على حساب Orbit الخاص بك، مرة واحدة بالضبط، مع إطلاق `number.ported`.
* **rejected** — رفض المشغل; يحمل `rejectionReason` الرمز المترجم. الرفض القابل للإصلاح يفتح نافذة `supplement` مدتها 7 أيام; وإرسال تعديل يوقف الصف في `supplement_submitted` حتى يقبله المشغل (عودة إلى `reviewing`) أو يرفضه مجددًا.
* **cancelled** — فرع نهائي لكل من port-in وport-out; لا ينطبق شيء آخر بعده.

ولدى LOA آلته الثلاثية الخاصة فوق ذلك: `loaSignatureStatus: draft → signed → submitted`، ولا تتقدم إلا إلى الأمام. LOA قديم التوقيع أو وثيقة خاطئة لا يمكن استبداله بعد `signed` — ألغِ عملية النقل وأعد الإرسال.

### Port-out (الرقم ينتقل خارج Orbit)

التقدم نفسه، لكنه مقيد أولًا بحالة الرقم نفسه: `released` و`suspended` و`parked` و`port_out_pending` كلها ترفض الإرسال بـ`409`، لذا فحص الحالة المسبق هو أول ما يُشغَّل. بعد تحويل FOC يملك المشغل الفائز الرقم — لا إلغاء ولا تراجع.

## 2. قائمة ما قبل الاستيراد — قبل أول استدعاء API

كل صنف رفض في القسم 5 يرجع إلى شيء في هذه القائمة. نفّذها قبل الإرسال لا بعده:

1. **سجل خدمة العميل (CSR)** من المشغل الخاسر — الاسم القانوني للكيان ورقم الحساب ورقم هاتف الفوترة (BTN) وعنوان الخدمة وأي PIN للتحويل، منسوخة حرفيًا. لا تعد كتابتها من الذاكرة; رفضا `NAME_MISMATCH` و`ADDRESS_MISMATCH` يأتيان شبه كليًا من حقول CSR مُخمَّنة.
2. **موقّع مخوّل** — شخص محدد بالاسم والصفة مخوّل بنقل الرقم وسيوقّع LOA. يجب أن يطابق اسمه هنا توقيع LOA وسجلات المشغل الخاسر تمامًا.
3. **نسخة LOA** — PDF حقيقي بدقة 200 dpi فأكثر، توقيع مؤرخ خلال 30 يومًا، وكل أرقام النقل مدرجة فيه. تتركّز رفضات `LOA_INVALID` هنا.
4. **PIN للخروج مضبوط مسبقًا على أرقامك في Orbit** — قبل إدخال أرقام جديدة، احمِ التي تملكها. `POST /numbers/:id/port-out-protection` مع PIN بطول 4–32 محرفًا يعني أن بيانات الاعتماد وحدها لا يمكنها أبدًا إرسال port-out; وأي شيء بلا PIN مطابق يُرفض بـ`401` قبل الاتصال بالمشغل. سجّل الـ PIN في مخزن أسرارك يوم ضبطه — لا مسار استرداد، فقط تدوير.
5. **معرفة قواعد البلد** — أرقام الولايات المتحدة/كندا تحصل على فحص نقل مباشر متزامن; وكل ما عداها يعيد `check-not-supported` وتكون الأهلية يدوية. الأرقام المجانية لا تستخدم مسار LNP إطلاقًا — تنتقل بتغيير RespOrg عبر `POST /numbers/porting/toll-free/resporg`.
6. **قرار الأهلية في اليد** — شغّل `POST /numbers/porting/pre-validate` واشترط `ready: true`. ذلك الاستدعاء الواحد يشغّل فحص النقل ويفحص حقول CSR معًا — صنفا الرفض في بوابة واحدة.

إذا بقي أي بند في القائمة بلا حسم، توقف. انضباط البوابات قبل أول إنشاء هو ما يمنع أن يتحول النقل الأول إلى رفض بعد ثلاثة أسابيع.

## 3. خطوة بخطوة — ترتيب المعالج

نموذج **Number Porting** في لوحة التحكم يمشي هذا الترتيب نفسه; والمكافئ عبر الواجهة البرمجية في الدليل. الترتيب نفسه هو ما يهم:

1. **أدخل الأرقام** (رقم E.164 واحد في كل سطر) ← شغّل بوابة ما قبل الإطلاق. إما مؤهل، أو لا تتقدم.
2. **املأ حقول المشغل + CSR** — المشغل الخاسر ورقم الحساب وPIN (لاسلكي الولايات المتحدة) والموقّع المخوّل وعنوان الخدمة — نسخ طبق الأصل عن CSR.
3. **نقطة توقف كمسودة** — `draft: true` تخزن الطلب محليًا بـ`staging: "draft"` ولا تمس أي مشغل. إعادة الإنشاء بالمجموعة نفسها من الأرقام idempotent، فلا تشعب نقطةَ التوقف إعادةُ زيارة أو تحديث للصفحة.
4. **ارفع LOA** داخل المنصة (`POST /numbers/porting/:id/loa`) — PDF/صورة أقل من 10MB.
5. **وقّع** (`.../loa/sign`) — اسم الموقّع وبريده الإلكتروني وإقراره. التوقيع مرتين يعيد `422`; وإذا وصلت إلى هنا مرتين فثمّة خطأ ما.
6. **أرسل** (`.../loa/submit`) — الفعل الوحيد الصالح في حالة `signed`. هذه هي اللحظة التي يبدأ فيها المشغل الخاسر مراجعة تستغرق 7–14 يوم عمل.
7. **تتبّع FOC** — `GET /numbers/porting/:id/timeline` يفرد الحالة المسطحة إلى مدخلات لكل مرحلة مع طوابع زمنية، ويعرض `focDate` في اللحظة التي يعين فيها المشغل واحدًا. راقب حتى `status: completed`.
8. **عند التحويل يُطلق `number.ported` مرة واحدة بالضبط** — يدخل الرقم دورة الحياة العادية (تجديد تلقائي، تحرير، إعادة إسناد) كما يفعل DID مشترى. والسلوك الجهّز لـSMS على رقم منقول يطابق سلوكه على رقم مشترى.
9. **لا تُركن مسودة أبدًا** — المسودة لا تبلغ المشغل ولا تحصل على FOC ولا تكتمل أبدًا. تعلّم لوحة التحكم هذه الحالة بالضبط كـ"draft — LOA pending". إذا كان الموقّع غير متاح أو CSR غير معروف، ألغِ المسودة وأعد إنشاءها عند الجاهزية; نقطة التوقف التي تتقادم بصمت هي أكثر الطرق شيوعًا لتعطل النقل الأول بهدوء.

إذا كانت العملية في `submitted` أو `reviewing` وثمّ خطأ فيها، ألغِها بـ`DELETE /numbers/porting/:id`. بعد `approved` يعيد الإلغاء `409` — ستكتمل العملية وفق جدول المشغل.

## 4. webhooks الحالة — بماذا تشترك

سجّل نقطة webhook واحدة واشترك في أحداث النقل بدل الاستطلاع. الأحداث التي تحرّك نقلًا أول:

| الحدث | متى يُطلق | ماذا تفعل به |
| - | - | - |
| `number.ported` | اكتمل الـ port-in وأصبح الرقم حيًا على حسابك. يُطلق مرة واحدة بالضبط. | بدّل التوجيه، وأرسل إشعار «أنتم أحياء»، وابدأ التجهيز التالي للنقل. |
| `porting.request.loa_signed` | التُقط توقيع LOA داخل المنصة. | حرّك قائمة التحقق الداخلية; وضع خطوة إرسال LOA في الطابور. |
| `porting.request.supplement_submitted` | أرسلت ملحقًا طلبه المشغل. | صفّر عدّاد نافذة التعديل; نبّه المشغّل الذي يملك الملف. |
| `porting.request.manual_review_required` | رجع النقل إلى مراجعة يدوية. | علّمه لمتابعة بشرية; لا تعد المحاولة تلقائيًا. |
| `porting.request.cancelled` | ألغيتَ أنت أو مدير الطلب. | أزلْه من اللوحة النشطة; وسجّل السبب. |
| `number.port_out.requested` | قُدّم port-out وقبله المشغل. | ابدأ عدّاد نافذة الإلغاء — بعد تحويل FOC لا ينطبق الإلغاء. |
| `number.port_out.cancelled` | أُلغي طلب port-out قبل التحويل. | أزل الرقم من لوحة «المغادرين». |

التسليم مرة واحدة على الأقل، لذا أزل التكرار اعتمادًا على `id` الحدث في مستقبلك. الأحداث تقود التحديثات اللحظية; ومسح يومي لـ`GET /numbers/porting` + `timeline` يصالح ما فاتك. الكتالوج الكامل في [Webhook Events](https://docs.orbit.devotel.io/webhooks/events.md); والتحقق من التوقيع في [Webhook Security](https://docs.orbit.devotel.io/webhooks/security.md).

## 5. الرفضات الشائعة — وإصلاح كل منها

يترجم `GET /numbers/porting/:id/timeline` رموز رفض المشغل إلى `summary` + `recommendedAction` بلغة واضحة موسومَين بخطورة: `operator_fixable` (أصلح بياناتك) أو `needs_carrier` (على المشغل الخاسر الحل) أو `fatal` (الرقم لا يمكن نقله إطلاقًا). الخمسة التي تعطّل النقل الأول:

| الرفض | لماذا يحدث | الإصلاح |
| - | - | - |
| `NAME_MISMATCH` | الاسم القانوني للكيان في الطلب يختلف عن CSR — غالبًا بسبب صيغة مُخمَّنة أو مختصرة ("Acme, Inc." مقابل "ACME INC"). | اسحب CSR وأرسل ملحقًا خلال نافذة التعديل ذات الـ7 أيام بالاسم منسوخًا حرفيًا. |
| `ADDRESS_MISMATCH` | عنوان الخدمة يختلف — أرقام الشقق، ZIP+4 مقابل ZIP، الاختصارات. | أرسل ملحقًا بعنوان CSR الدقيق مقتبسًا بلا تعديل عن المشغل. |
| `BTN_MISMATCH` | في الحسابات متعددة الخطوط، رقم هاتف الفوترة المرسل لا يطابق سجل فوترة المشغل الخاسر. | خذ BTN من CSR (حقل مستقل عن رقم حسابك) وأرسل ملحقًا. |
| رفض PIN (`401 PIN_MISMATCH` في port-out; رفض المشغل للـ PIN في port-in) | كان PIN التحويل من المشغل الخاسر غائبًا أو خاطئًا. | أكّد الـ PIN مع المشغل — لا يمكن للعملية التقدم بدونه. امنعه كليًا بضبط الحساب والـ PIN في قائمة ما قبل الاستيراد قبل الإرسال. |
| `LOA_INVALID` | ينقص LOA أرقام، أو غير موقّع، أو صورة لشاشة، أو موقّع قبل أكثر من 30 يومًا، أو نسخة دون حد الوضوح. | أعد LOA كـPDF حقيقي بدقة 200 dpi فأكثر مع قائمة أرقام كاملة وتوقيع مؤرخ خلال 30 يومًا; وألغِ النقل وأعد الإرسال بالوثيقة المصححة. |

أي رفض بخطورة `fatal` (`NUMBER_NOT_PORTABLE`) لا يستحق ملحقًا — على الرقم البقاء لدى المشغل الخاسر. رموز المشغل المجهولة تظهر مع نص المشغل الخام محفوظًا حتى تتمكن من التصعيد بالسبب الدقيق.

## 6. راقبه عبر الواجهة البرمجية — الاستدعاءات الأربعة، قابلة للتشغيل

القائمة أعلاه تسمّي نقاط النهاية; وهنا الحلقة القابلة للتنفيذ الدنيا. الاستدعاءات الأربعة نفسها بـcURL وNode.js وPython وGo وRuby وPHP، مقتصرة على ما يفعله كل منها (المرجع الكامل الذي تعكسه هذه المقتطفات هو [Number Porting](https://docs.orbit.devotel.io/numbers/porting.md)). استخدم مفتاح API بـ`numbers:read` لقراءتي check وtimeline، و`numbers:write` للإنشاء.

```bash cURL theme={null}
# Step 1 — pre-import check (numbers:read): one live carrier call.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/check \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "phoneNumber": "+14155551234", "currentCarrier": "Acme Telecom" }'
# Gate on: eligible === true

# Step 2 — create the port (numbers:write): draft: true checkpoints the row
# locally (staging: "draft") and dispatches nothing until the LOA is signed.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": ["+14155551234"],
    "currentCarrier": "Acme Telecom",
    "country": "US",
    "accountNumber": "998877",
    "authorizedSigner": "Jane Doe",
    "serviceAddress": {
      "line1": "1 Market St",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94105",
      "countryCode": "US"
    },
    "draft": true
  }'
# Response: { "data": { "id": "port_draft456", "staging": "draft", ... } } — keep the id

# Step 3 — FOC timeline read (numbers:read): a pure read on the stored row,
# no carrier call; echoes focDate the moment the carrier assigns one.
curl "https://api.orbit.devotel.io/api/v1/numbers/porting/port_draft456/timeline" \
  -H "X-API-Key: dv_live_sk_..."

# Step 4 — webhook receiver: one endpoint for every porting event.
curl -X POST https://api.orbit.devotel.io/api/v1/webhooks \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/orbit",
    "events": [
      "number.ported",
      "porting.request.loa_signed",
      "porting.request.supplement_submitted",
      "porting.request.manual_review_required",
      "porting.request.cancelled"
    ]
  }'
```

```typescript Node.js theme={null}
import { Devotel } from '@devotel-orbit/node';
const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY! });

// Step 1 — pre-import check: gate on eligible before any create
const check = await orbit.request("POST", "/numbers/porting/check", {
  phoneNumber: "+14155551234",
  currentCarrier: "Acme Telecom",
});
if (check.data.eligible !== true) {
  throw new Error(`Not portable: ${check.data.reasons.join(", ")}`);
}

// Step 2 — create the port: draft checkpoint while the LOA is outstanding
const port = await orbit.request("POST", "/numbers/porting", {
  numbers: ["+14155551234"],
  currentCarrier: "Acme Telecom",
  country: "US",
  accountNumber: "998877",
  authorizedSigner: "Jane Doe",
  serviceAddress: {
    line1: "1 Market St",
    city: "San Francisco",
    state: "CA",
    postalCode: "94105",
    countryCode: "US",
  },
  draft: true,
});
const portId = port.data.id;

// Step 3 — FOC timeline read: a stored-row read; safe to poll, no carrier call
const timeline = await orbit.request("GET", `/numbers/porting/${portId}/timeline`, undefined);
// timeline.data.focDate is set the moment the carrier assigns the cutover date

// Step 4 — webhook receiver: subscribe once, then react to events instead of polling
await orbit.request("POST", "/webhooks", {
  url: "https://yourapp.com/webhooks/orbit",
  events: [
    "number.ported",
    "porting.request.loa_signed",
    "porting.request.supplement_submitted",
    "porting.request.manual_review_required",
    "porting.request.cancelled",
  ],
});
```

```python Python theme={null}
from orbit_sdk import OrbitClient
client = OrbitClient.from_env()

# Step 1 — pre-import check: gate on eligible before any create
check = client.request("POST", "/numbers/porting/check", json_body={
    "phoneNumber": "+14155551234",
    "currentCarrier": "Acme Telecom",
})
if check["data"]["eligible"] is not True:
    raise SystemExit(f"Not portable: {', '.join(check['data']['reasons'])}")

# Step 2 — create the port: draft checkpoint while the LOA is outstanding
port = client.request("POST", "/numbers/porting", json_body={
    "numbers": ["+14155551234"],
    "currentCarrier": "Acme Telecom",
    "country": "US",
    "accountNumber": "998877",
    "authorizedSigner": "Jane Doe",
    "serviceAddress": {
        "line1": "1 Market St",
        "city": "San Francisco",
        "state": "CA",
        "postalCode": "94105",
        "countryCode": "US",
    },
    "draft": True,
})
port_id = port["data"]["id"]

# Step 3 — FOC timeline read: a stored-row read; safe to poll, no carrier call
timeline = client.request("GET", f"/numbers/porting/{port_id}/timeline")
# timeline["data"]["focDate"] is set the moment the carrier assigns the cutover date

# Step 4 — webhook receiver: subscribe once, then react to events instead of polling
client.request("POST", "/webhooks", json_body={
    "url": "https://yourapp.com/webhooks/orbit",
    "events": [
        "number.ported",
        "porting.request.loa_signed",
        "porting.request.supplement_submitted",
        "porting.request.manual_review_required",
        "porting.request.cancelled",
    ],
})
```

```go Go theme={null}
package main

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"strings"
)

// The four-call loop in Go stdlib. Each step is one request; print the body
// and keep data.id from step 2 for the later LOA calls.

func postJSON(key, path, body string) string {
	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1"+path, strings.NewReader(body))
	req.Header.Set("X-API-Key", key)
	req.Header.Set("Content-Type", "application/json")
	return do(req)
}

func get(key, path string) string {
	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1"+path, nil)
	req.Header.Set("X-API-Key", key)
	return do(req)
}

func do(req *http.Request) string {
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	return string(out)
}

func main() {
	key := os.Getenv("ORBIT_API_KEY")

	// Step 1 — pre-import check: gate on "eligible": true before any create.
	fmt.Println(postJSON(key, "/numbers/porting/check",
		`{"phoneNumber": "+14155551234", "currentCarrier": "Acme Telecom"}`))

	// Step 2 — create the port as a draft; read "id" from the printed body.
	fmt.Println(postJSON(key, "/numbers/porting", `{
	  "numbers": ["+14155551234"], "currentCarrier": "Acme Telecom", "country": "US",
	  "accountNumber": "998877", "authorizedSigner": "Jane Doe",
	  "serviceAddress": {"line1": "1 Market St", "city": "San Francisco", "state": "CA",
	    "postalCode": "94105", "countryCode": "US"},
	  "draft": true
	}`))

	// Step 3 — FOC timeline read; safe to poll, no carrier call.
	fmt.Println(get(key, "/numbers/porting/port_draft456/timeline"))

	// Step 4 — webhook receiver: subscribe once, react to events instead of polling.
	fmt.Println(postJSON(key, "/webhooks", `{
	  "url": "https://yourapp.com/webhooks/orbit",
	  "events": ["number.ported", "porting.request.loa_signed",
	    "porting.request.supplement_submitted", "porting.request.manual_review_required",
	    "porting.request.cancelled"]
	}`))

	// Multipart upload (the bulk-csv and LOA steps below) with the stdlib:
	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	f, _ := os.Open("port-in-batch.csv")
	part, _ := w.CreateFormFile("file", "port-in-batch.csv")
	io.Copy(part, f)
	w.Close()
	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/numbers/port-in/bulk-csv", &buf)
	req.Header.Set("X-API-Key", key)
	req.Header.Set("Content-Type", w.FormDataContentType())
	fmt.Println(do(req))
}
```

```ruby Ruby theme={null}
require "net/http"
require "json"
require "uri"

KEY = ENV.fetch("ORBIT_API_KEY")
BASE = URI("https://api.orbit.devotel.io/api/v1")

def call(method, path, body = nil, form = nil)
  uri = BASE.dup
  uri.path += path
  req = method == :get ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
  req["X-API-Key"] = KEY
  if form
    req.set_form([["file", File.open(form), filename: File.basename(form)]], "multipart/form-data")
  else
    req["Content-Type"] = "application/json"
    req.body = body && JSON.dump(body)
  end
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }.body
end

# Step 1 — pre-import check: gate on eligible before any create
check = JSON.parse(call(:post, "/numbers/porting/check",
  phoneNumber: "+14155551234", currentCarrier: "Acme Telecom"))
abort("Not portable: #{check["data"]["reasons"].join(", ")}") unless check["data"]["eligible"]

# Step 2 — create the port: draft checkpoint while the LOA is outstanding
port = JSON.parse(call(:post, "/numbers/porting", {
  numbers: ["+14155551234"], currentCarrier: "Acme Telecom", country: "US",
  accountNumber: "998877", authorizedSigner: "Jane Doe",
  serviceAddress: { line1: "1 Market St", city: "San Francisco", state: "CA",
                    postalCode: "94105", countryCode: "US" },
  draft: true,
}))
port_id = port["data"]["id"]

# Step 3 — FOC timeline read: a stored-row read; safe to poll, no carrier call
puts call(:get, "/numbers/porting/#{port_id}/timeline")

# Step 4 — webhook receiver: subscribe once, react to events instead of polling
puts call(:post, "/webhooks", {
  url: "https://yourapp.com/webhooks/orbit",
  events: %w[
    number.ported porting.request.loa_signed porting.request.supplement_submitted
    porting.request.manual_review_required porting.request.cancelled
  ],
})

# Multipart upload (bulk-csv / LOA steps below) with the stdlib:
puts call(:post, "/numbers/port-in/bulk-csv", nil, "port-in-batch.csv")
```

```php PHP theme={null}
<?php
$key = getenv("ORBIT_API_KEY");
$base = "https://api.orbit.devotel.io/api/v1";

function call(string $method, string $path, ?array $body = null, ?string $fileField = null): string {
    global $key, $base;
    $ch = curl_init($base . $path);
    $headers = ["X-API-Key: $key"];
    $opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method];
    if ($fileField !== null) {                      // multipart: bulk-csv / LOA uploads
        $opts[CURLOPT_POSTFIELDS] = ["file" => new CURLFile($fileField)];
    } elseif ($body !== null) {
        $headers[] = "Content-Type: application/json";
        $opts[CURLOPT_POSTFIELDS] = json_encode($body);
    }
    $opts[CURLOPT_HTTPHEADER] = $headers;
    curl_setopt_array($ch, $opts);
    $out = curl_exec($ch);
    curl_close($ch);
    return $out;
}

// Step 1 — pre-import check: gate on eligible before any create
$check = json_decode(call("POST", "/numbers/porting/check", [
    "phoneNumber" => "+14155551234", "currentCarrier" => "Acme Telecom",
]), true);
if (($check["data"]["eligible"] ?? false) !== true) {
    exit("Not portable: " . implode(", ", $check["data"]["reasons"]));
}

// Step 2 — create the port: draft checkpoint while the LOA is outstanding
$port = json_decode(call("POST", "/numbers/porting", [
    "numbers" => ["+14155551234"], "currentCarrier" => "Acme Telecom", "country" => "US",
    "accountNumber" => "998877", "authorizedSigner" => "Jane Doe",
    "serviceAddress" => ["line1" => "1 Market St", "city" => "San Francisco", "state" => "CA",
                         "postalCode" => "94105", "countryCode" => "US"],
    "draft" => true,
]), true);
$portId = $port["data"]["id"];

// Step 3 — FOC timeline read: a stored-row read; safe to poll, no carrier call
echo call("GET", "/numbers/porting/$portId/timeline");

// Step 4 — webhook receiver: subscribe once, react to events instead of polling
echo call("POST", "/webhooks", [
    "url" => "https://yourapp.com/webhooks/orbit",
    "events" => ["number.ported", "porting.request.loa_signed",
                 "porting.request.supplement_submitted", "porting.request.manual_review_required",
                 "porting.request.cancelled"],
]);

// Multipart upload (bulk-csv / LOA steps below) with the stdlib:
echo call("POST", "/numbers/port-in/bulk-csv", null, "port-in-batch.csv");
```

يُطلق الـ webhook POST حدث `porting.request.*` لكل حركة حالة بعد ذلك — في مستقبلك، أزل التكرار اعتمادًا على `id` الحدث وعامل `number.ported` بوصفه إشارة النجاح النهائية لا فحص حالة في الخط الزمني. نقطة التوقف كمسودة في الخطوة 2 مقصودة: إذا كان LOA موقّعًا وجاهزًا أصلًا، أزل `draft: true` لإرسال بدفعة واحدة. وفي الحالتين يعمل تسلسل LOA — رفع، توقيع، إرسال — على الـ`portId` المعاد ويبدأ الخط الزمني بالحركة.

استدعاء check محدود بـ20 طلبًا/الدقيقة; سجّل مستقبل الـ webhook مرة واحدة ولا تعد تسجيله داخل حلقة. من هنا تتولى قائمة التحقق في القسم 3: رفع LOA ← توقيع ← إرسال، ثم تتبع.

### ارفع قائمتك — CSV دفعة عبر multipart

تنقل أكثر من حفنة أرقام؟ تجاوز إنشاء كل سطر في الخطوة 2 وسلّم السجل كاملًا إلى `POST /numbers/port-in/bulk-csv` بصيغة `multipart/form-data` تحت الحقل `file`. احفظ القائمة كـ`port-in-batch.csv` بهذه الأعمدة بالضبط (الترتيب لا يهم، ومطابقة الترويسة لا تميز بين الأحرف):

```text theme={null}
phone_number,current_carrier_account_number,current_carrier_name,billing_name,billing_address
+14155551234,998877,Acme Telecom,Jane Doe,"1 Market St, San Francisco, CA 94105, US"
+14155555678,998877,Acme Telecom,Jane Doe,"1 Market St, San Francisco, CA 94105, US"
```

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/port-in/bulk-csv \
  -H "X-API-Key: dv_live_sk_..." \
  -F "file=@port-in-batch.csv"
```

```typescript Node.js theme={null}
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append(
  "file",
  new Blob([await readFile("port-in-batch.csv")], { type: "text/csv" }),
  "port-in-batch.csv",
);

const res = await fetch("https://api.orbit.devotel.io/api/v1/numbers/port-in/bulk-csv", {
  method: "POST",
  headers: { "X-API-Key": process.env.ORBIT_API_KEY! },
  body: form,
});
console.log(await res.json());
```

```python Python theme={null}
import os

import requests

with open("port-in-batch.csv", "rb") as fh:
    res = requests.post(
        "https://api.orbit.devotel.io/api/v1/numbers/port-in/bulk-csv",
        headers={"X-API-Key": os.environ["ORBIT_API_KEY"]},
        files={"file": ("port-in-batch.csv", fh, "text/csv")},
        timeout=30,
    )
print(res.json())
```

كل صف صالح يصبح طلب نقل خاصًا به، ويبلغ الاستجابة عن جانبي الدفعة — `request_ids` لقيادة تسلسل LOA لكل صف، و`validation_errors` للصفوف التي تُصلح وتُرفع مجددًا (الصفوف المكررة الجارية تُتخطى، لذا إعادة الرفع المصحح آمنة):

```json 200 theme={null}
{
  "data": {
    "request_ids": ["port_a1b2c3", "port_d4e5f6"],
    "validation_errors": [
      { "row": 14, "field": "phone_number", "error": "Not a valid E.164 number" }
    ]
  }
}
```

الحدود: 2 MB لكل ملف، و1000 صف لكل طلب، وميزانية معالجة 30 ثانية، وطلبان في الدقيقة لكل منظمة. اقتبس أي حقل يحتوي فاصلة (`billing_address` يحتاجها شبه دائمًا); وترويسة ينقصها عمود مطلوب تجهض الرفع كاملًا بـ`422` وتسمّي الأعمدة الناقصة.

### قدّم LOA — رفع، توقيع، إرسال

يعمل LOA على معرف النقل المعاد من الخطوة 2 (أو من كل صف دفعة) كثلاثة استدعاءات متتابعة، يحرّك كل واحد `loaSignatureStatus` خطوة إلى الأمام. جهّز PDF حقيقيًا بدقة 200 dpi فأكثر مع توقيع مؤرخ خلال 30 يومًا وكل أرقام النقل مدرجة — تتركّز رفضات `LOA_INVALID` في هذه النقاط الثلاث نفسها.

```bash cURL theme={null}
# 1. Upload the LOA (PDF / JPEG / PNG / HEIC, up to 10MB) under the file field
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_draft456/loa \
  -H "X-API-Key: dv_live_sk_..." \
  -F "file=@loa.pdf"

# 2. Record the in-platform signature — moves the LOA from draft to signed
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_draft456/loa/sign \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "signerName": "Jane Doe",
    "signerEmail": "jane@acme.com",
    "acknowledgement": "I am authorized to request this number be ported to Orbit and the information above is accurate."
  }'

# 3. Forward the signed LOA to the losing carrier — the only call valid in signed
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_draft456/loa/submit \
  -H "X-API-Key: dv_live_sk_..."
```

```typescript Node.js theme={null}
import { readFile } from "node:fs/promises";
import { Devotel } from '@devotel-orbit/node';
const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY! });

const portId = "port_draft456"; // from step 2 of the four-call loop

// 1. Upload — the SDK request() is JSON-only, so send the multipart via fetch
const form = new FormData();
form.append(
  "file",
  new Blob([await readFile("loa.pdf")], { type: "application/pdf" }),
  "loa.pdf",
);
await fetch(`https://api.orbit.devotel.io/api/v1/numbers/porting/${portId}/loa`, {
  method: "POST",
  headers: { "X-API-Key": process.env.ORBIT_API_KEY! },
  body: form,
});

// 2. Sign — draft → signed; signing twice returns 422
await orbit.request("POST", `/numbers/porting/${portId}/loa/sign`, {
  signerName: "Jane Doe",
  signerEmail: "jane@acme.com",
  acknowledgement:
    "I am authorized to request this number be ported to Orbit and the information above is accurate.",
});

// 3. Submit — signed → submitted; the carrier review clock starts here
await orbit.request("POST", `/numbers/porting/${portId}/loa/submit`, undefined);
```

```python Python theme={null}
import os

import requests
from orbit_sdk import OrbitClient

client = OrbitClient.from_env()
port_id = "port_draft456"  # from step 2 of the four-call loop

# 1. Upload — multipart under the file field
with open("loa.pdf", "rb") as fh:
    requests.post(
        f"https://api.orbit.devotel.io/api/v1/numbers/porting/{port_id}/loa",
        headers={"X-API-Key": os.environ["ORBIT_API_KEY"]},
        files={"file": ("loa.pdf", fh, "application/pdf")},
        timeout=30,
    )

# 2. Sign — draft → signed; signing twice returns 422
client.request("POST", f"/numbers/porting/{port_id}/loa/sign", json_body={
    "signerName": "Jane Doe",
    "signerEmail": "jane@acme.com",
    "acknowledgement": "I am authorized to request this number be ported to Orbit and the information above is accurate.",
})

# 3. Submit — signed → submitted; the carrier review clock starts here
client.request("POST", f"/numbers/porting/{port_id}/loa/submit")
```

يعيد كل استدعاء طلب النقل مع LOA حالةً واحدة أبعد:

```json Upload — 201 theme={null}
{ "data": { "id": "port_draft456", "loaSignatureStatus": "draft" } }
```

```json Sign — 200 theme={null}
{ "data": { "id": "port_draft456", "loaSignatureStatus": "signed" } }
```

```json Submit — 200 theme={null}
{ "data": { "id": "port_draft456", "loaSignatureStatus": "submitted" } }
```

آلة الحالات تتقدم إلى الأمام فقط: بمجرد أن يكون LOA في `signed` أو `submitted` يُرفض رفع بديل — ألغِ النقل وأعد الإرسال بالوثيقة المصححة. ومن `submitted` يتولى الخط الزمني في الخطوة 3 الأمر.

## See Also

* [Port a number end-to-end](https://docs.orbit.devotel.io/guides/port-numbers.md) — الدليل الذي ترسمه هذه الصفحة; كل نقطة نهاية محلولة بالترتيب.
* [Port out safely](https://docs.orbit.devotel.io/guides/port-out-numbers.md) — الدليل المتناظر لنقل الأرقام خارج Orbit، يشمل حماية PIN وانضباط نافذة الإلغاء.
* [Number Porting endpoint sequence](https://docs.orbit.devotel.io/numbers/porting.md) — المرجع الأقصر لكل نقطة نهاية.
* [Number Lifecycle](https://docs.orbit.devotel.io/numbers/lifecycle.md) — ما ينطبق على الرقم بعد اكتمال النقل.
* [Webhook Events](https://docs.orbit.devotel.io/webhooks/events.md) — الكتالوج الكامل للأحداث.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.