At Amazon I designed, built, tested, and deployed the public phone-number APIs for Amazon Connect, in Java, released globally, for Connect users provisioning numbers for their contact centers. In Connect a phone number is a cloud resource with an ARN, tags, IAM permissions, an asynchronous provisioning workflow, and a quota rule limiting how often numbers can be claimed and released. Everything in this post comes from the public contract.
Amazon Connect is AWS’s cloud contact center. A call reaches an agent only if the customer has a number to dial, so provisioning numbers is the first step in every Connect deployment. That lifecycle is managed by the family of public APIs I worked on: ClaimPhoneNumber, ImportPhoneNumber, DescribePhoneNumber, and UpdatePhoneNumber, along with SearchAvailablePhoneNumbers and ReleasePhoneNumber.
| API | Call | What it does |
|---|---|---|
| ClaimPhoneNumber | POST /phone-number/claim |
Provision an available number to an instance or traffic distribution group |
| ImportPhoneNumber | POST /phone-number/import |
Bring a number you already own in AWS End User Messaging into Connect |
| DescribePhoneNumber | GET /phone-number/{PhoneNumberId} |
Details plus the status of the last claim/import/update |
| UpdatePhoneNumber | PUT /phone-number/{PhoneNumberId} |
Re-point the number at a different instance or traffic distribution group |
The sections below cover the decisions encoded in that contract.
Phone numbers are modeled as resources
The APIs do not key on the E.164 string, +18005550100. ClaimPhoneNumber returns two identifiers:
{
"PhoneNumberId": "a1b2c3d4-...",
"PhoneNumberArn": "arn:aws:connect:us-east-1:123456789012:phone-number/a1b2c3d4-..."
}
and every other API in the family keys on PhoneNumberId or the ARN, never the number itself. The string is not a stable identity. Numbers get released, sit in a cooldown pool, and get claimed by someone else later; the same digits can refer to different resources at different times, owned by different AWS accounts. The UUID identifies one claim of the number.
Modeling the number as a resource makes the standard AWS mechanisms apply: the ARN can be referenced in IAM policies, the Tags map (up to 50 entries, standard AWS constraints) feeds cost allocation and access control, and the resource appears in audit tooling alongside every other AWS resource. A bare string supports none of these.
Asynchronous provisioning
ClaimPhoneNumber returns HTTP 200 with an ID in well under a second, before provisioning has completed. Provisioning touches telephony inventory and routing configuration outside the request path and can take longer than an HTTP timeout allows. The API is therefore asynchronous by contract: the 200 means accepted, and the docs direct callers to DescribePhoneNumber for the outcome. The status is returned as an enum value, with a message on failure:
stateDiagram-v2
[*] --> IN_PROGRESS : ClaimPhoneNumber / ImportPhoneNumber / UpdatePhoneNumber (HTTP 200)
IN_PROGRESS --> CLAIMED : provisioning workflow succeeds
IN_PROGRESS --> FAILED : provisioning workflow fails (Message says why)
CLAIMED --> IN_PROGRESS : UpdatePhoneNumber
Failure is reported as the FAILED value of PhoneNumberStatus. Provisioning fails after the HTTP call has returned, so no request remains on which to return a 500. The failure is recorded in PhoneNumberStatus next to a Message giving the reason, and stays readable through DescribePhoneNumber for as long as the resource exists.
Idempotency and ClientToken
Every mutating API in the family takes a ClientToken, and the AWS SDK generates one if the caller does not supply it. Retrying a call with the same token returns the same operation; reusing a token for a different operation returns a 409 IdempotencyException.
The failure mode without a token: a client claims a number, the response times out in the network, the client retries, and it now holds two claimed phone numbers while its own records show one. Phone numbers are carrier-leased and billed monthly, so the duplicate carries a recurring cost. Timeouts are routine and clients wrap calls in retries, so the API makes retries safe at the contract level. The docs link Amazon’s essay on the pattern, Making retries safe with idempotent APIs.
Instance and traffic distribution group ownership
ClaimPhoneNumber and UpdatePhoneNumber both take exactly one of InstanceId or TargetArn. A number can belong to a single Connect instance, or to a traffic distribution group, which spans two AWS Regions so that inbound calls survive a regional failover.
The traffic distribution group case constrains which identifier a caller can use. From the DescribePhoneNumber docs: in the Region where the group was created, either the UUID or the full ARN resolves; in the group’s alternate Region, only the full ARN resolves, and a UUID returns ResourceNotFoundException. A bare UUID resolves only where the resource’s home records live. The ARN embeds the Region and the account, so it can be resolved from either Region. For a resource that spans Regions, the caller’s Region is part of the API semantics, and the contract has to state which identifiers are valid where.
The scope of UpdatePhoneNumber
UpdatePhoneNumber re-points a number at a different instance or traffic distribution group. The docs carry a warning box: the API switches only the number. The contact flow the number was attached to, which controls what happens when someone calls, is not moved, and the caller must attach a flow in the target.
A flow belongs to an instance and references queues, prompts, and Lambda functions that exist in that instance. Migrating the flow is not possible in the general case, because those referenced resources do not exist in the target. The API performs only the re-pointing operation, whose semantics are well defined, and documents the boundary the caller has to handle.
The 180-day rule
Both claim and import document a churn limit: an account can claim and release up to 200% of its phone number quota in a rolling 180-day window, and past that it is blocked from claiming until 180 days after the oldest release, unless it opens a support ticket.
The resource behind the API is physical. Phone numbers come from carrier inventory; they are finite within a country and prefix, and a released number has to sit in cooldown before reuse, because the previous owner’s customers are still calling it. Unlimited churn by one account would exhaust a country’s available inventory. The quota puts that scarcity in the contract; without it the constraint reaches other accounts as claims failing for lack of available numbers.
Calling it from Java
The service is written in Java, as is the client below. It covers the full lifecycle, including the asynchronous contract (production code would bound the polling loop and handle FAILED explicitly):
ConnectClient connect = ConnectClient.create();
ClaimPhoneNumberResponse claim = connect.claimPhoneNumber(r -> r
.instanceId(instanceId)
.phoneNumber("+18005550100") // from SearchAvailablePhoneNumbers
.phoneNumberDescription("Support line")
.clientToken(UUID.randomUUID().toString())); // retries now safe
// 200 means accepted, not done. Poll for the terminal state.
String id = claim.phoneNumberId();
ClaimedPhoneNumberSummary summary;
do {
Thread.sleep(1_000);
summary = connect.describePhoneNumber(r -> r.phoneNumberId(id))
.claimedPhoneNumberSummary();
} while (summary.phoneNumberStatus().status() == PhoneNumberWorkflowStatus.IN_PROGRESS);
if (summary.phoneNumberStatus().status() == PhoneNumberWorkflowStatus.FAILED) {
throw new IllegalStateException(summary.phoneNumberStatus().message());
}
Constraints on a public AWS API contract
A published contract cannot be revised. Once a field, enum value, or error shape is public, customer production systems depend on it. Fields can be added; existing ones cannot be changed or removed. Every name in the response syntax was reviewed on the basis that it could not be corrected after release.
The error types are part of the contract. IdempotencyException on token reuse, ResourceInUseException on a conflicting update, and ThrottlingException at the rate limit are the shapes clients build retry and reconciliation logic against. An undocumented error is a defect even when the behavior behind it is correct.
The “Important” callouts in these docs carry the design information: poll DescribePhoneNumber for the outcome, the contact flow does not migrate, use the full ARN in a traffic distribution group’s alternate Region, and the 180-day rule. Each marks a point where a constraint in the underlying system reaches the API surface, and none of them are expressed by the request and response syntax alone. Reading those callouts is the most direct way to recover the design constraints a mature API was built under.