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

# API Integration

> Learn how to integrate Hum into your application using our API

# Building with the Hum API

The Hum API provides endpoints for discovering available internet service providers, plans, and pricing at addresses in the United States. Most address lookups start asynchronously and complete through polling. Results are returned immediately only when the building was looked up recently. Use `qualify_status` to determine whether to poll or display the results.

The partner API provides availability lookup and reporting. Residents place orders through the Hum widget checkout; the partner API does not provide an order-placement endpoint. The [Hum Broadband MCP](/mcp/overview) has a separate schema-version 4 contract for agent-mediated validation and ordering. Its tools, identifiers, and fulfillment states do not extend or reinterpret this partner API.

<Info>
  Expect `qualify_status: "pending"` on the first lookup of an address, and plan your integration around polling `GET /sessions/{token}/services/internet` for the result. If the building was looked up recently, provider data may be available immediately in the session creation response.
</Info>

## Integration Overview

Integrating with Hum is a simple three-step process:

1. **Authenticate** with your API key
2. **Create a session** with the service address
3. **Check `qualify_status`**, then poll for completion or display an immediate result

## Quick Start Guide

Start in Sandbox with the examples below. To go live, switch the base URL to `https://api.letshum.com` and use your Production key; see [Environments and API Keys](/environments).

<Steps>
  <Step title="Get Your API Key">
    See [Environments and API Keys](/environments) to request an API key and choose the matching environment.
  </Step>

  <Step title="Create a Session and Check Results">
    Make a POST request to create a session with the service address. Expect to poll for provider results, and check `qualify_status` to determine whether polling is needed or results are ready immediately.

    ```bash cURL theme={null}
    curl -X POST https://api-sandbox.letshum.com/sessions \
      -H "Authorization: Bearer hum_sandbox_XXXXXXXXXXXXXXXXXXXXXXXXXX" \
      -H "Content-Type: application/json" \
      -d '{
        "street1": "29090 Tiffany Drive E",
        "street2": "Apt 4B",
        "zip": "48034"
      }'
    ```

    ```json Success Response theme={null}
    {
      "message": "Session created successfully.",
      "request_status": "ok",
      "qualify_status": "available",
      "data": [
        {
          "provider_id": "130317",
          "provider_name": "Xfinity",
          "provider_icon": null,
          "provider_logo": "https://cdn.example.com/providers/xfinity-logo.png",
          "provider_promo": {},
          "button_label": "xfinity.com",
          "telephone": "+18332981431",
          "url": "https://affiliate.example.com/xfinity?session=SESSION_TOKEN_PLACEHOLDER",
          "url_promo": {},
          "min_plan_price": {
            "currency": "USD",
            "amount_cents": 4000
          },
          "offerings": [
            {
              "technology": "Cable",
              "max_download_speed": 1200,
              "max_upload_speed": 35
            }
          ],
          "product_catalog": [
            {
              "category": "internet",
              "category_name": "Internet Service",
              "category_description": "High-speed internet access plans",
              "products": [
                {
                  "id": "003aa129-5b35-443e-8ea6-c3a33977b5ab",
                  "sku": "130317-INT-CBL-02",
                  "name": "300 Mbps",
                  "category": "internet",
                  "category_name": "Internet Service",
                  "technology": "cable",
                  "position": 2,
                  "description": "Great for everyday working, streaming, and learning. Price guaranteed for 60 months. No contract required.",
                  "select_type": "radio",
                  "download_speed": "300",
                  "upload_speed": "100",
                  "data_limit": "Unlimited",
                  "channel_count": 0,
                  "streaming_apps": [],
                  "is_required_to_checkout": true,
                  "is_contract_required": false,
                  "is_modem_router_included": true,
                  "is_bundle_qualifier": false,
                  "bundle_discounts": {},
                  "is_local_checkout": true,
                  "required_with_plans": [],
                  "included_with_plans": [],
                  "initial_term_discount_months": 60,
                  "second_term_discount_months": null,
                  "only_available_with_plans": [],
                  "hum_rank": 69,
                  "pricing": {
                    "extra_data_fee": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "professional_installation_fee": {
                      "amount_cents": 5000,
                      "currency": "USD"
                    },
                    "self_installation_fee": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "activation_fee": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "initial_term_discount": {
                      "amount_cents": 1500,
                      "currency": "USD"
                    },
                    "second_term_discount": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "third_term_discount": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "autopay_discount": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "paperless_billing_discount": {
                      "amount_cents": 0,
                      "currency": "USD"
                    },
                    "combined_autopay_paperless_discount": {
                      "amount_cents": 1000,
                      "currency": "USD"
                    },
                    "net_monthly_price": {
                      "amount_cents": 5500,
                      "currency": "USD"
                    },
                    "gross_monthly_fee": {
                      "amount_cents": 8000,
                      "currency": "USD"
                    }
                  },
                  "max_quantity": 1,
                  "product_promo": {},
                  "info": "5-year price lock guarantee"
                }
              ]
            }
          ]
        }
      ],
      "meta": {
        "session_token": "SESSION_TOKEN_PLACEHOLDER",
        "session_status": "open",
        "session_params": {
          "street1": "29090 Tiffany Dr E",
          "street2": "Apt 4B",
          "city": "Southfield",
          "state": "MI",
          "zip": "48034",
          "latitude": "42.50189",
          "longitude": "-83.29528",
          "campaign_id": null
        },
        "service_address": "29090 Tiffany Dr E Apt 4B, Southfield, MI 48034-4540",
        "mdu": true,
        "agent_status": {
          "geocoding": "matched",
          "internet": "matched",
          "checkout": "pending"
        },
        "created_at": "2026-07-13T12:26:15.618-04:00",
        "updated_at": "2026-07-13T12:26:15.618-04:00",
        "responded_at": "2026-07-13T16:26:16.278Z",
        "hum_data_set": "26011015"
      }
    }
    ```

    <Check>
      Branch on `qualify_status`: expect `pending` and poll for completion, use `data` for `available`, treat only `no_service` as no service, and also poll for `retry_later`.
    </Check>
  </Step>

  <Step title="Display Available Internet Plans">
    When `qualify_status` is `available`, the response includes all available providers and their offerings. You can display this information to your users:

    * Provider name and contact details
    * Available plans with speeds and pricing
    * Direct links to provider signup pages
    * Technology types (DSL, Cable, Fiber, etc.)

    <Tip>
      Polling is the expected path for most lookups. When `qualify_status` is `pending` or `retry_later`, poll the Internet service availability endpoint for the result.
    </Tip>
  </Step>
</Steps>

## Handle Qualification Status

The optional `qualify_status` field distinguishes a completed lookup from one that is not ready yet:

**The answer is ready:**

* `available`: providers were found and are returned in `data`.
* `no_service`: no providers serve this address. `data` is `[]`. You can act on this now, but Hum re-checks periodically, so re-validate it if you store it long term.

**The answer is not ready, keep polling:**

* `pending`: the lookup has not finished. `data` is `[]`.
* `retry_later`: a transient upstream problem. `data` is `[]`.

**Something went wrong:**

* `failed`: the lookup errored for this address. `data` is `[]`.

A `pending` lookup finishes as one of three values: `available` if providers were found, `no_service` if the lookup completed and found none, or `failed` if it errored. `retry_later` arises separately and does not follow from `pending`. Those three are the states that should end your polling loop.

For `pending` or `retry_later`, use the session token to poll `GET /sessions/{token}/services/internet` with a bounded retry. Only `no_service` means the completed lookup found no service; never interpret `data: []` by itself as a no-service result.

### Polling duration

Addresses that have not been looked up recently, including most single-family addresses, start with `pending` and resolve through polling. They commonly take 20 to 30 seconds to resolve, and occasionally longer. While the status is `pending` or `retry_later`, poll `GET /sessions/{token}/services/internet` every 2 seconds. Give up after about 90 seconds of polling. Giving up means the answer is still unknown, not that no service is available. Re-check the address later if the result matters. Only `no_service` means no service.

## Optional: Retrieve Session Details Later

If you need to retrieve session information later, you can use the session token:

```bash cURL theme={null}
curl -X GET https://api-sandbox.letshum.com/sessions/YOUR_SESSION_TOKEN \
  -H "Authorization: Bearer hum_sandbox_XXXXXXXXXXXXXXXXXXXXXXXXXX"
```

This endpoint returns session details and normalized address metadata. Its `data` object contains only the session token:

```json Session Details Response theme={null}
{
  "message": "Session agents have successfully matched the service address. Please proceed.",
  "request_status": "ok",
  "data": {
    "session_token": "SESSION_TOKEN_PLACEHOLDER"
  },
  "meta": {
    "session_token": "SESSION_TOKEN_PLACEHOLDER",
    "session_status": "open",
    "session_params": {
      "street1": "29090 Tiffany Dr E",
      "street2": "Apt 4B",
      "city": "Southfield",
      "state": "MI",
      "zip": "48034",
      "latitude": "42.50189",
      "longitude": "-83.29528",
      "campaign_id": null
    },
    "service_address": "29090 Tiffany Dr E Apt 4B, Southfield, MI 48034-4540",
    "mdu": true,
    "agent_status": {
      "geocoding": "matched",
      "internet": "matched",
      "checkout": "pending"
    },
    "created_at": "2026-07-13T12:26:15.618-04:00",
    "updated_at": "2026-07-13T12:26:15.618-04:00",
    "responded_at": "2026-07-13T16:26:16.900Z",
    "hum_data_set": "26011015"
  }
}
```

While the session is open, retrieve its provider offerings again with `GET /sessions/{token}/services/internet`. See [Get Internet Service Availability](/api-reference/service-availability/get-internet-service-availability) in the API Reference.

For analytics session detail, `GET /analytics/sessions/{id}` accepts either the session UUID returned by the analytics index or the session token returned when the session was created.

## Session Lifecycle

Sessions stay open until you close them with `DELETE /sessions/{token}`. Close a session when its lookup is finished; see [Close Session](/api-reference/sessions/close-session).

## Authentication

All API requests require authentication using your API key. Include it in the Authorization header:

```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```

Use authenticated `GET /ping` as the safest first call before creating a session. It verifies that the selected environment accepts your key; see [Environments and API Keys](/environments#verify-your-key).

## Error Handling

The API uses HTTP status codes to indicate success or failure. Standard validation, authentication, and session errors return this format:

```json Error Response Format theme={null}
{
  "message": "Invalid session token.",
  "request_status": "warning",
  "request_id": "3f1c3c07-785d-4c59-97dd-25cb38f670d7"
}
```

The `message` describes the result, `request_status` provides informational severity, and `request_id` is a support reference for the request. Validation responses may also include an `errors` object with field-specific details.

The `request_id` is best-effort because request logging failures cannot be allowed to break the request. A logging failure can leave no matching API log row. When reporting an error, quote the `request_id` and the request timestamp so support can bound the lookup by date.

<Warning>
  Use the HTTP status code to determine whether a request succeeded. The `request_status` field is informational.
</Warning>

Successful session metadata identifies the Hum data set with a numeric string such as `"26011015"`. Responses raised before an API controller handles the request may use a different format, as documented in the API Reference.

### Common Error Scenarios

<AccordionGroup>
  <Accordion title="400 Bad Request">
    **Common causes:**

    * Missing session parameters
    * Unpermitted session parameters
    * Invalid session token

    **Resolution:** Verify the request body uses supported session fields and confirm that the session token is correct.
  </Accordion>

  <Accordion title="401 Unauthorized - Authentication Issues">
    **Common causes:**

    * Missing or invalid API key
    * Expired API key
    * Invalid authentication header format

    **Resolution:** Verify your API key is correct and properly formatted in the Authorization header.
  </Accordion>

  <Accordion title="415 Unsupported Media Type">
    **Common causes:**

    * Unsupported or incorrect request content type

    **Resolution:** Send session creation requests with `Content-Type: application/json`.
  </Accordion>

  <Accordion title="422 Unprocessable Entity - Validation Failed">
    **Common causes:**

    * Missing a valid address combination: `street1` + `zip`, `street1` + `city` + `state`, or `street1` + `city` + `zip`
    * Invalid address, state, or ZIP format

    ```json Example theme={null}
    {
      "message": "Session could not be created.",
      "request_status": "warning",
      "request_id": "3f1c3c07-785d-4c59-97dd-25cb38f670d7",
      "errors": {
        "base": [
          "Provide either (street1 and zip), (street1, city, and state), or (street1, city, and zip)"
        ]
      }
    }
    ```

    **Resolution:** Use the `errors` object to correct the address fields before retrying.
  </Accordion>

  <Accordion title="503 Service Unavailable - Address Validation">
    When address validation is temporarily unavailable during session creation, this response is an exception to the common error envelope.

    ```json Response theme={null}
    {
      "address_validation_unavailable": true
    }
    ```

    **Resolution:** Respect the `Retry-After` response header and retry later.
  </Accordion>

  <Accordion title="429 Too Many Requests - Rate Limiting">
    **Common causes:**

    * Rate limit exceeded

    **Resolution:** Wait for the delay specified by the `Retry-After` header before retrying.
  </Accordion>

  <Accordion title="500 Internal Server Error - Server Issues">
    **Common causes:**

    * Unexpected server-side failure

    **Resolution:** Retry the request after a brief delay. Contact support if the issue persists.
  </Accordion>
</AccordionGroup>

For detailed error codes and handling, refer to the [API Reference](/api-reference/sessions/create-new-session) documentation.

## Best Practices

<CardGroup cols={2}>
  <Card title="Address Validation" icon="location-dot">
    Provide accurate address data upfront to ensure the best results. Include complete address details including street number, street name, city, state, and ZIP code.

    <Tip>
      Use address validation services before sending requests to minimize errors and improve match rates.
    </Tip>
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation">
    Implement robust error handling for common scenarios like invalid addresses, no service availability, or API rate limits.

    <Warning>
      Always provide clear feedback to users when errors occur, using the error messages and status codes from the API response.
    </Warning>
  </Card>

  <Card title="Session Management" icon="key">
    Store and manage session tokens appropriately. Each session represents a unique address lookup, and tokens can be used to retrieve session details later.
  </Card>

  <Card title="Performance Optimization" icon="gauge-high">
    Consider caching provider data for frequently requested addresses to improve response times and reduce API calls.

    <Tip>
      Process provider data from the session creation response when `qualify_status` is `available`; poll when it is `pending` or `retry_later`.
    </Tip>
  </Card>
</CardGroup>

## Example Integration

Here's a complete example showing how to integrate the Hum API in different programming languages:

<CodeGroup>
  ```javascript Node.js theme={null}
  async function findInternetProviders(address) {
    try {
      const response = await fetch('https://api-sandbox.letshum.com/sessions', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.HUM_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(address)
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      let data = await response.json();
      const sessionToken = data.meta.session_token;

      const retryableStatuses = new Set(['pending', 'retry_later']);
      const maxPollAttempts = 45; // 2 seconds x 45 attempts = about 90 seconds.

      for (let attempt = 0; retryableStatuses.has(data.qualify_status) && attempt < maxPollAttempts; attempt += 1) {
        await new Promise(resolve => setTimeout(resolve, 2000));

        const pollResponse = await fetch(
          `https://api-sandbox.letshum.com/sessions/${sessionToken}/services/internet`,
          {
            headers: {
              'Authorization': `Bearer ${process.env.HUM_API_KEY}`
            }
          }
        );

        if (!pollResponse.ok) {
          throw new Error(`HTTP error while polling! status: ${pollResponse.status}`);
        }

        data = await pollResponse.json();
      }

      let providers;
      if (retryableStatuses.has(data.qualify_status)) {
        // The result is still unknown. Do not record this as no service available; the address can be re-checked later.
        providers = null;
      } else if (data.qualify_status === 'available') {
        providers = data.data;
      } else if (data.qualify_status === 'no_service') {
        providers = [];
      } else if (data.qualify_status === 'failed') {
        throw new Error('Provider qualification failed for this address');
      } else if (data.qualify_status === undefined && data.data.length > 0) {
        providers = data.data;
      } else {
        throw new Error(`Unexpected qualify_status: ${data.qualify_status ?? 'missing'}`);
      }
      
      if (providers === null) {
        console.log(`Provider qualification is still in progress at ${address.street1}`);
      } else {
        console.log(`Found ${providers.length} providers at ${address.street1}`);
      }
      
      return {
        providers,
        sessionToken,
        serviceAddress: data.meta.service_address
      };
      
    } catch (error) {
      console.error('Error finding providers:', error);
      throw error;
    }
  }

  // Usage
  const address = {
    street1: "1001 Woodward Ave",
    city: "Detroit",
    state: "MI",
    zip: "48226"
  };

  findInternetProviders(address)
    .then(result => {
      if (result.providers !== null) {
        result.providers.forEach(provider => {
          console.log(`${provider.provider_name}: Starting at $${provider.min_plan_price.amount_cents / 100}/mo`);
        });
      }
    })
    .catch(error => {
      console.error('Failed to find providers:', error);
    });
  ```

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

  def find_internet_providers(address):
      try:
          headers = {
              'Authorization': f'Bearer {os.getenv("HUM_API_KEY")}',
              'Content-Type': 'application/json'
          }

          response = requests.post(
              'https://api-sandbox.letshum.com/sessions',
              headers=headers,
              json=address
          )
          
          response.raise_for_status()
          data = response.json()
          session_token = data['meta']['session_token']

          retryable_statuses = {'pending', 'retry_later'}
          max_poll_attempts = 45  # 2 seconds x 45 attempts = about 90 seconds.

          for _ in range(max_poll_attempts):
              if data.get('qualify_status') not in retryable_statuses:
                  break

              time.sleep(2)
              response = requests.get(
                  f'https://api-sandbox.letshum.com/sessions/{session_token}/services/internet',
                  headers=headers
              )
              response.raise_for_status()
              data = response.json()

          qualify_status = data.get('qualify_status')
          if qualify_status in retryable_statuses:
              # The result is still unknown. Do not record this as no service available; the address can be re-checked later.
              providers = None
          elif qualify_status == 'available':
              providers = data['data']
          elif qualify_status == 'no_service':
              providers = []
          elif qualify_status == 'failed':
              raise RuntimeError('Provider qualification failed for this address')
          elif qualify_status is None and data['data']:
              providers = data['data']
          else:
              raise RuntimeError(f'Unexpected qualify_status: {qualify_status or "missing"}')
          
          if providers is None:
              print(f"Provider qualification is still in progress at {address['street1']}")
          else:
              print(f"Found {len(providers)} providers at {address['street1']}")
          
          return {
              'providers': providers,
              'session_token': session_token,
              'service_address': data['meta']['service_address']
          }
          
      except requests.exceptions.RequestException as error:
          print(f'Error finding providers: {error}')
          raise

  # Usage
  address = {
      'street1': '1001 Woodward Ave',
      'city': 'Detroit',
      'state': 'MI',
      'zip': '48226'
  }

  try:
      result = find_internet_providers(address)
      if result['providers'] is not None:
          for provider in result['providers']:
              price = provider['min_plan_price']['amount_cents'] / 100
              print(f"{provider['provider_name']}: Starting at ${price}/mo")
  except Exception as error:
      print(f'Failed to find providers: {error}')
  ```

  ```php PHP theme={null}
  <?php

  function findInternetProviders($address) {
      $apiKey = $_ENV['HUM_API_KEY'];
      
      $curl = curl_init();
      
      curl_setopt_array($curl, [
          CURLOPT_URL => 'https://api-sandbox.letshum.com/sessions',
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST => true,
          CURLOPT_HTTPHEADER => [
              'Authorization: Bearer ' . $apiKey,
              'Content-Type: application/json'
          ],
          CURLOPT_POSTFIELDS => json_encode($address)
      ]);
      
      $response = curl_exec($curl);
      $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
      curl_close($curl);
      
      if ($httpCode !== 201) {
          throw new Exception("HTTP error! status: $httpCode");
      }
      
      $data = json_decode($response, true);
      $sessionToken = $data['meta']['session_token'];

      $retryableStatuses = ['pending', 'retry_later'];
      $maxPollAttempts = 45; // 2 seconds x 45 attempts = about 90 seconds.

      for ($attempt = 0; in_array($data['qualify_status'] ?? null, $retryableStatuses, true) && $attempt < $maxPollAttempts; $attempt++) {
          sleep(2);

          $curl = curl_init();
          curl_setopt_array($curl, [
              CURLOPT_URL => "https://api-sandbox.letshum.com/sessions/$sessionToken/services/internet",
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_HTTPHEADER => [
                  'Authorization: Bearer ' . $apiKey
              ]
          ]);

          $response = curl_exec($curl);
          $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
          curl_close($curl);

          if ($httpCode !== 200) {
              throw new Exception("HTTP error while polling! status: $httpCode");
          }

          $data = json_decode($response, true);
      }

      $qualifyStatus = $data['qualify_status'] ?? null;
      if (in_array($qualifyStatus, $retryableStatuses, true)) {
          // The result is still unknown. Do not record this as no service available; the address can be re-checked later.
          $providers = null;
      } elseif ($qualifyStatus === 'available') {
          $providers = $data['data'];
      } elseif ($qualifyStatus === 'no_service') {
          $providers = [];
      } elseif ($qualifyStatus === 'failed') {
          throw new Exception('Provider qualification failed for this address');
      } elseif ($qualifyStatus === null && count($data['data']) > 0) {
          $providers = $data['data'];
      } else {
          throw new Exception('Unexpected qualify_status: ' . ($qualifyStatus ?? 'missing'));
      }
      
      if ($providers === null) {
          echo "Provider qualification is still in progress at " . $address['street1'] . "\n";
      } else {
          echo "Found " . count($providers) . " providers at " . $address['street1'] . "\n";
      }
      
      return [
          'providers' => $providers,
          'sessionToken' => $sessionToken,
          'serviceAddress' => $data['meta']['service_address']
      ];
  }

  // Usage
  $address = [
      'street1' => '1001 Woodward Ave',
      'city' => 'Detroit',
      'state' => 'MI',
      'zip' => '48226'
  ];

  try {
      $result = findInternetProviders($address);
      if ($result['providers'] !== null) {
          foreach ($result['providers'] as $provider) {
              $price = $provider['min_plan_price']['amount_cents'] / 100;
              echo $provider['provider_name'] . ": Starting at $" . $price . "/mo\n";
          }
      }
  } catch (Exception $error) {
      echo 'Failed to find providers: ' . $error->getMessage() . "\n";
  }

  ?>
  ```
</CodeGroup>

## Next Steps

<CardGroup cols={3}>
  <Card title="API Reference" icon="book" href="/api-reference/sessions/create-new-session">
    Explore detailed endpoint documentation, parameter specifications, and response schemas.
  </Card>

  <Card title="Postman Collection" icon="circle-play" href="/postman">
    Import ready-made requests for Sandbox or Production.
  </Card>

  <Card title="Widget Integration" icon="puzzle-piece" href="/widget-integration/overview">
    Try our no-code widget solution for quick integration without custom development.
  </Card>
</CardGroup>


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