Phone Validation API Integration Testing: A Complete Guide for Developers

The first time a phone validation API broke our signup flow in production, it wasn’t because the API was down. It was because we’d never tested what our code did when the API returned a 429. A burst of traffic tripped the rate limit, our integration threw an unhandled exception, and real customers got a blank error page mid-registration. The lookup worked fine in every demo. We just hadn’t tested the parts that aren’t the happy path.

That’s the gap I want to close here. Wiring up a phone validation call takes an afternoon. Making the integration survive contact with production takes a test suite that exercises the ugly cases on purpose. Below is how I’d structure that, with examples against CheckThatPhone’s validation features.

What actually breaks in production

The lookup call itself is rarely the problem. The problems live around it:

  • False rejections, where a valid customer gets bounced out of registration
  • Bad data slipping through, so invalid numbers land in your database anyway
  • API spend creeping up because retries fire too aggressively on errors
  • Validation getting bypassed entirely, which opens the door to fraudulent accounts
  • Slow or unhelpful failures that the user sees as a broken form

Every one of these is catchable in a test. None of them shows up if you only test that a known-good number returns valid: true.

Set up a test environment that doesn’t burn quota

Before you write a single test case, separate the two worlds. Unit tests should never touch the real API. Integration tests should, but rarely. Mixing them is how you wake up to a surprise bill from your CI runner looping a validation call ten thousand times.

Build a real test dataset

I keep a fixture file with numbers that cover the cases that bite:

  • Valid numbers in several formats: 555-123-4567, (555) 123-4567, +15551234567
  • Invalid numbers: wrong length, bad area codes, disconnected lines
  • Edge cases: toll-free, VoIP, ported numbers
  • Canadian numbers, with the formatting variations they bring

CheckThatPhone handles both US and Canadian numbers, so cover both regions with the right country codes. The formatting variants matter more than people expect, because a normalization bug in your own code usually surfaces here, not in the API.

Mock the API for unit tests

For unit tests, mock the response so you’re testing your logic, not the network:

// Example using Jest
const mockValidationResponse = {
  valid: true,
  phone_number: "+15551234567",
  line_type: "mobile",
  carrier: "Verizon Wireless",
  is_ported: false,
  location: {
    city: "New York",
    state: "NY",
    timezone: "America/New_York"
  }
};

jest.mock('checkthatphone-sdk', () => ({
  validatePhone: jest.fn().mockResolvedValue(mockValidationResponse)
}));

Now you can drive different response shapes through your code without spending a credit. This is where you exercise the branches: what does your handler do with a null carrier, a missing location, a valid: false?

The test scenarios worth your time

1. Basic validation

Start with the obvious checks, then move on quickly. They catch wiring mistakes, not much else.

def test_valid_mobile_number():
    response = checkthatphone.validate("+15551234567")
    assert response['valid'] == True
    assert response['line_type'] == 'mobile'
    
def test_invalid_number_format():
    response = checkthatphone.validate("123")
    assert response['valid'] == False
    assert 'error' in response

2. Line type detection

CheckThatPhone tells you whether a number is mobile, landline, VoIP, or toll-free. If you send SMS verification codes, this is the field that decides whether a signup can even receive the code. Test the branch:

def test_voip_number_handling():
    response = checkthatphone.validate("+15551234567")
    if response['line_type'] == 'voip':
        # Your business logic for VoIP numbers
        assert should_allow_voip_registration() == True

VoIP is the one I’d watch. Plenty of legitimate users carry a Google Voice number, and plenty of throwaway fraud accounts do too. Whatever you decide, decide it in code and pin it with a test.

3. Carrier lookup

If carrier drives any routing or business rule, test it explicitly:

test('carrier-specific routing', async () => {
  const result = await checkThatPhone.validate('+15551234567');
  
  if (result.carrier === 'T-Mobile') {
    // Test T-Mobile specific logic
    expect(shouldUsePriorityQueue(result)).toBe(true);
  }
});

4. Number portability

The is_ported flag tells you a number moved between carriers. That matters when your logic assumes the carrier on file is current:

def test_ported_number_carrier_accuracy():
    response = checkthatphone.validate("+15551234567")
    if response['is_ported']:
        # Ensure you're using current carrier, not original
        assert response['carrier'] != response['original_carrier']

5. Geolocation

If you gate by region, verify the location data lines up with what you allow:

test('geolocation restrictions', async () => {
  const result = await checkThatPhone.validate('+15551234567');
  const allowedStates = ['NY', 'CA', 'TX'];
  
  if (result.location && result.location.state) {
    expect(allowedStates).toContain(result.location.state);
  }
});

Error handling is the part everyone skips

This is where my production story started, so it gets its own section.

API error responses

Test what your code does with each failure mode, not just success:

  • Rate limiting: 429 Too Many Requests
  • Authentication failures: 401 Unauthorized
  • Network timeouts: connection errors
  • Malformed JSON or a partial response
def test_rate_limit_handling():
    with mock.patch('requests.post') as mock_post:
        mock_post.return_value.status_code = 429
        
        result = validate_with_retry("+15551234567")
        assert retry_count > 0
        assert result is not None  # Verify retry logic worked

Timeouts

Pick a timeout, then test that you actually fail cleanly when you blow past it. An integration that hangs forever is worse than one that errors fast.

const options = {
  timeout: 5000,  // 5 second timeout
  retries: 3
};

test('timeout handling', async () => {
  await expect(
    validateWithTimeout('+15551234567', { timeout: 1 })
  ).rejects.toThrow('Timeout');
});

Performance and load

Response time

If you have an SLA, hold the integration to it:

import time

def test_api_response_time():
    start = time.time()
    response = checkthatphone.validate("+15551234567")
    duration = time.time() - start
    
    assert duration < 1.0  # Should respond within 1 second
    assert response['valid'] is not None

Batch validation

When you validate many numbers at once, confirm you get a result back for every input. A silent drop in a batch is easy to miss:

def test_batch_validation():
    numbers = ["+15551234567", "+15559876543", "+14165551234"]
    results = checkthatphone.validate_batch(numbers)
    
    assert len(results) == len(numbers)
    assert all('valid' in r for r in results)

Before you ship

Run through this list before the integration goes live:

  • All test scenarios passing consistently
  • Error handling tested for every API error code you can hit
  • Logging and monitoring configured
  • API credentials in a secret store, never hardcoded
  • Rate limiting and retry logic implemented and tested
  • Timeout values set for your use case
  • At least one integration test against the real API, not only mocks
  • Docs reviewed at CheckThatPhone docs
  • A pricing plan sized for your expected volume

Fitting this into CI

A few rules that have kept our pipeline cheap and reliable:

  1. Split the suites. Mocked unit tests run on every commit. Real-API integration tests run on a schedule or before release, not on every push.
  2. Use a separate test key, so test traffic never muddies your production usage stats.
  3. Watch for flaky tests. A test that fails intermittently is often telling you something about the API or your retry logic.
  4. Cache responses for expensive integration checks so CI isn’t paying for the same lookup repeatedly.

The integration that broke our signup flow now has a test that fires a 429 and asserts the user sees a retry, not a stack trace. Write the test for the failure that scares you most, then work backward. Start with the docs at CheckThatPhone docs, and pick a pricing plan that matches the volume you expect to test against.

Start validating phone numbers today

CheckThatPhone provides real-time carrier, line type, portability, and deliverability data for US & Canada numbers in a single API call.