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.

Prerequisites

  • A Surge platform and API key
  • Your business EIN (US) or CBN (Canada)
  • A published privacy policy and terms of service URL

Step 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...
import Surge from "@surgeapi/node";

const surge = new Surge();

const account = await 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",
    },
  },
});

console.log(account.id); // acct_01j...
curl -X POST https://api.surge.app/accounts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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"
      }
    }
  }'

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.

Step 2 (optional): 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)
const status = await surge.accounts.retrieveStatus(
  "acct_01jrzhe8d9enptypyx360pcmxj",
  { capabilities: ["local_messaging"] },
);

console.log(status.capabilities.local_messaging.status);
console.log(status.capabilities.local_messaging.fields_needed);
curl "https://api.surge.app/accounts/ACCOUNT_ID/status?capabilities=local_messaging" \
  -H "Authorization: Bearer YOUR_API_KEY"

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.

Step 3 (optional): 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"},
    },
)
await surge.accounts.update("acct_01jrzhe8d9enptypyx360pcmxj", {
  organization: {
    contact: { phone_number: "+18015551234" },
    address: { line1: "123 Main St" },
  },
});
curl -X PATCH https://api.surge.app/accounts/ACCOUNT_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organization": {
      "contact": {
        "phone_number": "+18015551234"
      },
      "address": {
        "line1": "123 Main St"
      }
    }
  }'

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

Step 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"
const campaign = await 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",
  },
);

console.log(campaign.id);     // cpn_01j...
console.log(campaign.status); // "in_review"
curl -X POST https://api.surge.app/accounts/ACCOUNT_ID/campaigns \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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"
  }'

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.

Step 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
const phoneNumber = await surge.phoneNumbers.purchase(
  "acct_01jrzhe8d9enptypyx360pcmxj",
  {
    type: "local",
    area_code: "801",
  },
);

console.log(phoneNumber.number); // +18015559876
curl -X POST https://api.surge.app/accounts/ACCOUNT_ID/phone_numbers \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "local",
    "area_code": "801"
  }'
{
  "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