Surge

Register via API

This walkthrough takes you from a fresh Surge platform to a fully registered campaign with a dedicated phone number. If you submit all required fields in step 1, you can skip steps 2 and 3 entirely.

Note

Sole proprietor without an EIN? This walkthrough assumes you have a tax ID (EIN or CBN). If you don't, follow the Sole Proprietor Path — different fields, no EIN required, and dashboard-first.

Prerequisites

  • Surge platform and API key: Create the platform at hq.surge.app and copy a key from API Keys.
  • Business tax ID: Your EIN (US) or CBN (Canada).
  • Privacy policy and terms of service URLs: Both must be live and publicly indexable when reviewers visit them.

1. Create an account

An account represents a brand in carrier terms. Create one with your business details:

from surge import Surge

surge = Surge()

account = surge.accounts.create(
    name="Acme Corp",
    organization={
        "type": "private_corporation",
        "registered_name": "Acme Corp Inc.",
        "identifier_type": "ein",
        "identifier": "12-3456789",
        "website": "https://acme.com",
        "industry": "technology",
        "regions_of_operation": ["usa_and_canada"],
        "email": "hello@acme.com",
        "address": {
            "line1": "123 Main St",
            "locality": "Salt Lake City",
            "region": "UT",
            "postal_code": "84101",
            "country": "US",
        },
        "contact": {
            "first_name": "Jane",
            "last_name": "Smith",
            "email": "jane@acme.com",
            "phone_number": "+18015551234",
            "title": "ceo",
        },
    },
)

print(account.id)  # acct_01j...

The response includes your new account ID. Save it; all subsequent requests use it.

{
  "id": "acct_01jrzhe8d9enptypyx360pcmxj",
  "name": "Acme Corp",
  "brand_name": null,
  "time_zone": null,
  "organization": {
    "type": "private_corporation",
    "registered_name": "Acme Corp Inc.",
    "identifier_type": "ein",
    "identifier": "12-3456789",
    "website": "https://acme.com",
    "industry": "technology",
    "regions_of_operation": ["usa_and_canada"],
    "email": "hello@acme.com",
    "address": {
      "line1": "123 Main St",
      "locality": "Salt Lake City",
      "region": "UT",
      "postal_code": "84101",
      "country": "US"
    },
    "contact": {
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "jane@acme.com",
      "phone_number": "+18015551234",
      "title": "ceo"
    }
  }
}

If you're running this for each of your own customers, create one account per customer. See One Account per Customer for the reasoning.

Optional: 2. Check what's missing

The status endpoint tells you whether your account is ready to register a campaign and what fields are still needed:

status = surge.accounts.retrieve_status(
    "acct_01jrzhe8d9enptypyx360pcmxj",
    capabilities=["local_messaging"],
)

print(status.capabilities["local_messaging"].status)
print(status.capabilities["local_messaging"].fields_needed)

The response includes a fields_needed array listing any missing or invalid fields:

{
  "capabilities": {
    "local_messaging": {
      "status": "incomplete",
      "fields_needed": ["organization.contact.phone_number", "organization.address.line1"],
      "errors": []
    }
  }
}

An empty fields_needed array and a "ready" status means you can proceed to create a campaign.

Optional: 3. Fill the gaps

If fields_needed is non-empty, patch the account to add the missing information. The field names in the response are dot-delimited paths into the account object:

surge.accounts.update(
    "acct_01jrzhe8d9enptypyx360pcmxj",
    organization={
        "contact": {"phone_number": "+18015551234"},
        "address": {"line1": "123 Main St"},
    },
)

Repeat steps 2 and 3 until capabilities.local_messaging.status is "ready".

4. Create a campaign

A campaign tells carriers what you'll send and how users opted in. Every field here is read by human reviewers, so write for clarity.

campaign = surge.campaigns.create(
    "acct_01jrzhe8d9enptypyx360pcmxj",
    description="Transactional and marketing messages for Acme Corp customers who have opted in via our checkout page.",
    use_cases=["marketing"],
    includes=["links"],
    consent_flow='Users enter their phone number on our checkout page. A checkbox labeled "I agree to receive text messages from Acme Corp" is present and unchecked by default. Required disclosures appear beneath the checkbox: "Message frequency varies. Msg&data rates apply. Reply STOP to opt out."',
    message_samples=[
        "You are now opted in to messages from Acme Corp. Frequency varies. Msg&data rates apply. Reply STOP to opt out.",
        "Acme Corp: Your order #12345 has shipped. Track it here: https://acme.com/track",
        "Acme Corp: 20% off this weekend only. Shop now: https://acme.com/sale. Reply STOP to opt out.",
    ],
    volume="low",
    privacy_policy_url="https://acme.com/privacy",
    terms_and_conditions_url="https://acme.com/terms",
)

print(campaign.id)      # cpn_01j...
print(campaign.status)  # "in_review"

The campaign goes straight to in_review:

{
  "id": "cpn_01jrzhe8d9enptypyx360pcmxl",
  "status": "in_review",
  "description": "Transactional and marketing messages for Acme Corp customers who have opted in via our checkout page.",
  "use_cases": ["marketing"],
  "includes": ["links"],
  "volume": "low",
  "consent_flow": "Users enter their phone number on our checkout page...",
  "message_samples": [
    "You are now opted in to messages from Acme Corp. Frequency varies. Msg&data rates apply. Reply STOP to opt out.",
    "Acme Corp: Your order #12345 has shipped. Track it here: https://acme.com/track",
    "Acme Corp: 20% off this weekend only. Shop now: https://acme.com/sale. Reply STOP to opt out."
  ],
  "privacy_policy_url": "https://acme.com/privacy",
  "terms_and_conditions_url": "https://acme.com/terms"
}

A few things about campaign fields:

  • volume: "low" allows up to 2,000 SMS segments/day to T-Mobile. "high" allows up to 200,000/day, depending on the trust score assigned by The Campaign Registry.
  • use_cases: The types of messages you'll send. Common values: marketing, customer_care, account_notification, two_factor_authentication. See the Schema Reference for the full list.
  • message_samples: 2–5 examples of what you'll actually send. The first should be a compliance opt-in confirmation. Use real brand names, not placeholders.

When the status changes to active, Surge fires a campaign.approved webhook event. The campaign is approved, but a number isn't ready to send production messages until it's also been attached to the campaign. Surge starts attaching numbers automatically as soon as the campaign goes active; the phone_number.attached_to_campaign webhook fires per number once the attachment completes. Wait for that event before sending real traffic from a given number.

5. Purchase a phone number

You can buy a number while your campaign is in review. You just can't send production messages from it until two things have happened: the campaign is active, and the number has been attached to it. After the campaign is approved, attachment runs automatically; you'll receive a phone_number.attached_to_campaign webhook for each number once it's ready.

phone_number = surge.phone_numbers.purchase(
    "acct_01jrzhe8d9enptypyx360pcmxj",
    type="local",
    area_code="801",
)

print(phone_number.number)  # +18015559876
{
  "id": "pn_01jrzhe8d9enptypyx360pcmxm",
  "number": "+18015559876",
  "type": "local",
  "campaign_id": null
}

campaign_id is null because the campaign isn't active yet. Surge attaches phone numbers to campaigns automatically. Two things trigger attachment:

  • Purchase while a campaign is active: Surge attaches the number immediately.
  • Campaign becomes active after purchase: Surge attaches all existing unattached numbers on the account when the campaign is approved.

When attachment completes, Surge fires a phone_number.attached_to_campaign webhook event. Once you receive that event, the number can send production traffic.

What's next