Porting a number
A plumber has their number on the side of a van, on a website, in eight years of customers' phones. When they switch provider they expect to keep it. That expectation is reasonable, it is protected by regulation, and satisfying it involves one company asking a competitor to hand over an asset that competitor would rather keep.
This turns out to be unlike almost any other integration you will write, and the differences are instructive.
You are asking a competitor for a favour they must grant
The provider losing the number has no commercial interest in the port succeeding. They are obliged to co-operate, and they do, but nothing about the process is designed for your convenience. You submit a request in their format, to their timetable, and they validate it against records you cannot see.
The consequence is that failures are not exceptions in your code — they are a normal, expected, high-volume category of outcome, and most of them are semantic rather than technical. The request was well-formed and got rejected because a name is recorded differently at the other end, or the address on file does not match, or the account number quoted belongs to a different line on the same premises.
Software that treats "rejected" as an error path underestimates it. Rejection is a state the system lives in for days at a time, and users need to be able to see why, act on it, and resubmit.
Documents are part of the protocol
Most integrations move data. This one moves paperwork.
A port is authorised by a Letter of Authority — a document signed by the customer saying they want this to happen. It has to be generated with the right details, presented to the customer, signed, stored, and submitted. Some ports additionally need proof that the customer owns the number, which means accepting a document upload, tracking whether it arrived, and attaching it to the right request.
So the system needs opinions about things most backends never think about: what to show when document generation fails, how to represent a request that is complete except for a signature, how to record that a document exists but has not yet been accepted by the other side. Those are all states, they all need to be visible, and none of them are captured by the usual pending/success/failure vocabulary.
Different providers want different variants of the same document, and not every field applies to every provider — one needs blanks where another needs values. Jurisdiction matters too: an Irish port is a different process with a different document from a UK one, even through the same carrier.
Timescales are measured in days
A port is not a transaction. It is a request that sits with somebody else for a week or more, with intermediate states you learn about by polling or by receiving notifications you did not schedule.
That has two practical effects.
You need completion syncing, not just submission. Getting the request accepted is the easy half. Knowing that it completed — so the number starts working on your side at the right moment, the customer is told, and the old service is released — is a separate ongoing job per carrier.
Long-running requests need visibility. One of the more useful things we added was simply surfacing ports first submitted more than a month ago. Nothing clever — but a request that has been quietly stuck for five weeks is invisible in a list sorted by recency, and the people who can unstick it are not the people watching the queue.
Bulk changes the shape
A business moving offices does not port one number. It ports a range, sometimes with attached numbers that come along with the main line, sometimes retaining a subset and releasing the rest. Everything above then applies to a set rather than an item: one authorisation covering many numbers, partial rejections, and a request that is simultaneously succeeding and failing.
Designing that as "loop over the single-number case" does not survive contact with a partial failure, because the authorising document covers the whole set and cannot be re-issued for the remainder without going back to the customer.
The regulator is in the data model
One detail I like, because it is so specific to the domain: there is a published list of numbers that must never be used as a calling line identity — ranges reserved, unallocated, or otherwise not valid as an originating number. Handling it correctly means knowing where it applies and where it does not, and those distinctions come from regulation rather than from anything you can reason out from first principles.
That is the recurring feature of this work. A lot of the rules are not derivable. You cannot work out what a valid port request looks like by thinking carefully; you find out by reading the specification, by asking the carrier, and by having submissions rejected until the shape is right. Engineering intuition is close to useless, and being unbothered by that is most of the skill.
What I took from it
Model rejection as a first-class state. When a large fraction of requests will be refused for reasons outside your control, "error handling" is the wrong frame. Rejection needs a reason a human can act on, a route back to a resubmission, and a place in the interface.
Long-running processes need an age, not just a status. "Pending" is fine on day one and alarming on day forty, and nothing in a status field distinguishes them. Surfacing duration is often more valuable than adding states.
When the counterparty is a competitor, assume nothing is for your benefit. The format, the timetable and the error messages are all designed around their systems. Build for that rather than being repeatedly surprised by it.
Some correctness is legislated, not derived. In most software, if a rule seems arbitrary you are probably missing the reason. In regulated telecoms the rule is frequently just arbitrary — it is a decision somebody made, written down, and your job is to implement it exactly, not to understand why.
Also in this series
- The address that has to be right — two models for a 999 routing address
- Someone else's phone bill — toll fraud, and escalating rather than blocking