https://a.storyblok.com/f/270183/1368x665/9cf2bb9c91/25oct_dev-blog_sim-swap-go.jpg

Detect SIM Swap Fraud with Go and Vonage

Published on October 23, 2025

Time to read: 5 minutes

Most online accounts still rely on SMS for two-factor authentication (2FA). But what happens if a fraudster convinces your carrier to transfer your number to their SIM (SIM swap attack)? Suddenly, those security codes meant to protect you become the very tool used against you.

A SIM swap check doesn’t replace 2FA; it tells your app whether SMS is still a safe channel to use. In this tutorial, we’ll show you how to integrate Vonage’s Identity Insights API with Go (Golang) to detect SIM swaps in real time and protect your users from account hijacking.

>> TL;DR: Get the full working code on GitHub.

Prerequisites

To follow along, you’ll need:

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Project Setup

Create a Vonage Application

  • To create an application, go to the Create an Application page on the Vonage Dashboard, and define a Name for your Application.

  • If needed, click on "generate public and private key". A private key (.key file) will be generated. Download and store it securely. This key is needed for authentication when making API requests. Note: Private keys will not work unless the application is saved.

  • Choose the capabilities you need (e.g., Voice, Messages, RTC, etc.) and provide the required webhooks (e.g., event URLs, answer URLs, or inbound message URLs). These will be described in the tutorial.

  • To save and deploy, click "Generate new application" to finalize the setup. Your application is now ready to use with Vonage APIs.

The Identity Insights API depends on live mobile network data to detect SIM swaps. Enabling Network Registry in your Vonage Application grants access to this data source. In production, this requires operator approval, which can take time, but the Playground mode lets you start testing immediately.

In order to do that, create a Vonage application in the Vonage Dashboard with: 

  1. Network Registry capability enabled, and select “Playground”

  2. Generate a private.key, you will need to move this to the root of your directory in the following section

Set Up Your Go App

In your terminal, run the following commands to create a project directory and the necessary files:

mkdir go_sim_swap_checker
cd go_sim_swap_checker
touch main.go .env

>> You can now move your downloaded private.key into the go_sim_swap_checker directory

Install Dependencies

In your project directory, initialize the Go module and install the required packages:

go mod init sim_swap_checker
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
go get github.com/google/uuid
  • golang-jwt: Handles JWT token generation for API authentication

  • godotenv: Loads environment variables from a .env file

  • uuid: Generates unique identifiers for each JWT, ensuring every token can be securely distinguished

Configure Your .env File

Open the .env file in your text editor and add the following:

VONAGE_APPLICATION_ID=your_application_id
VONAGE_PRIVATE_KEY_PATH=./private.key
PHONE_NUMBER=+990123455
DEFAULT_HOURS=240

Add your Vonage Application ID, a default phone number, and the time period in hours for the SIM swap check.

The Vonage Virtual Operator provides 9 valid phone numbers that you can use for testing, along with one that gives an “unknown” response.

You can see in the Identity Insights API Reference that the Sim swap insight relies on a single parameter called period, we’ll call it hour, and set the default as 240. For the virtual operator, values below 500 will return a response that the SIM has not been swapped. Values over 500, will result in a positive Sim Swap API response.

Writing the Go Script to Check for a SIM Swap

Step 1: Set Up the Imports and Environment

Before we can make API requests, we need to bring in Go packages for HTTP requests, JSON handling, and environment variable management. We’ll also load our credentials from a .env file so we’re not hardcoding sensitive information in the script.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"github.com/joho/godotenv"
)

func loadEnv() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}
}

Step 2: Authenticate with JWT

The Identity Insights API uses JWT (JSON Web Token) authentication. A JWT proves to Vonage that your application has permission to make the request.

We’ll define a Credentials type to hold the Vonage Application ID and the private key, add a NewCredentials constructor to load them, and then use a single-purpose method GenerateJWT(ttl) to create and sign the token. This keeps file I/O, validation, and token creation cleanly separated and easy to test.

type Credentials struct {
  ApplicationID string
  PrivateKeyPEM []byte
}

func NewCredentials(applicationID, privateKeyPath string) (*Credentials, error) {
  if applicationID == "" {
    return nil, fmt.Errorf("missing VONAGE_APPLICATION_ID")
  }
  if privateKeyPath == "" {
    return nil, fmt.Errorf("missing VONAGE_PRIVATE_KEY_PATH")
  }
  pem, err := os.ReadFile(privateKeyPath)
  if err != nil {
    return nil, fmt.Errorf("read private key: %w", err)
  }
  return &Credentials{ApplicationID: applicationID, PrivateKeyPEM: pem}, nil
}

func (c *Credentials) GenerateJWT(ttl time.Duration) (string, error) {
  key, err := jwt.ParseRSAPrivateKeyFromPEM(c.PrivateKeyPEM)
  if err != nil {
    return "", fmt.Errorf("parse rsa key: %w", err)
  }

  now := time.Now()
  claims := jwt.MapClaims{
    "iat":            now.Unix(),
    "exp":            now.Add(ttl).Unix(),
    "jti":            uuid.NewString(),         
    "application_id": c.ApplicationID,
  }

  tok := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
  signed, err := tok.SignedString(key)
  if err != nil {
    return "", fmt.Errorf("sign jwt: %w", err)
  }
  return signed, nil
}

Step 3: Define Sim Swap Structs

To handle the API’s JSON response cleanly in Go, we’ll define a set of structs that mirror the structure of the Identity Insights API’s SIM Swap data.

These structs will let us parse the API response directly into typed Go objects, making it easy to work with the data:

type SimSwapStatus struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

type SimSwapDetails struct {
	Swapped       *bool         `json:"swapped,omitempty"`
	LastSimSwapAt string        `json:"latest_sim_swap_at,omitempty"`
	Status        SimSwapStatus `json:"status"`
}

type SimSwapResponse struct {
	RequestID string `json:"request_id"`
	Insights  struct {
		SimSwap SimSwapDetails `json:"sim_swap"`
	} `json:"insights"`
}

Step 4: Define Core Sim Swap Function

With authentication sorted out, let’s connect to the API. The checkSimSwap function brings everything together: it accepts a phone number, a time window (in hours), the API URL, and the JWT token you generated earlier.

Here’s what’s going on:

  1. Prepare the request: build a JSON body that includes the phone number, the fraud-prevention purpose, and the SIM swap period.

  2. Send it securely:  use http.NewRequest to create a POST request, attach the JWT in the Authorization header, and set the content type.

  3. Parse the response: decode the JSON into our SimSwapResponse struct so we can work with typed fields instead of raw JSON.

  4. Interpret the result: based on the status code (OK, NO_COVERAGE, INTERNAL_ERROR)

func checkSimSwap(phoneNumber, hours, apiURL, jwtToken string) {
  reqBody := map[string]interface{}{
    "phone_number": phoneNumber,
    "purpose":      "FraudPreventionAndDetection",
    "insights": map[string]interface{}{
      "sim_swap": map[string]interface{}{
        "period": hours,
      },
    },
  }

  body, err := json.Marshal(reqBody)
  if err != nil {
    log.Fatal("Error marshaling request body:", err)
  }

  req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
  if err != nil {
    log.Fatal("Error creating request:", err)
  }

  req.Header.Set("Authorization", "Bearer "+jwtToken)
  req.Header.Set("Content-Type", "application/json")

  client := &http.Client{}
  resp, err := client.Do(req)
  if err != nil {
    log.Fatal("Error sending request:", err)
  }
  defer resp.Body.Close()

  var res SimSwapResponse
  if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
    log.Fatalf("Error decoding API response: %v", err)
  }

  switch res.Insights.SimSwap.Status.Code {
  case "OK":
    if res.Insights.SimSwap.Swapped != nil {
      if *res.Insights.SimSwap.Swapped {
        t, err := time.Parse(time.RFC3339, res.Insights.SimSwap.LastSimSwapAt)
        formatted := res.Insights.SimSwap.LastSimSwapAt
        if err == nil {
          formatted = t.Format("January 2, 2006 at 3:04 PM")
        }
        fmt.Printf("\n🚨 ALERT: SIM swap detected!\nLast swap occurred around: %s\n", formatted)
      } else {
        fmt.Println("\n✅ No SIM swap detected. The phone number appears to be secure.")
      }
    } else {
      fmt.Println("\n⚠️ Status was OK but swap status was missing.")
    }
  case "NO_COVERAGE":
    fmt.Println("\nℹ️ This phone number is not supported by the SIM swap check service.")
  case "INTERNAL_ERROR":
    fmt.Println("\n❌ An unexpected error occurred while checking the phone number. Please try again later.")
  default:
    fmt.Printf("\n❌ An unknown error occurred: %s\n", res.Insights.SimSwap.Status.Message)
  }
}

Step 5: CLI Wrapper for Interactivity

To make the script practical, we’ll wrap it in a main() function so you can run it directly. The CLI wrapper prompts the user for a phone number and a custom time period, or falls back to the default. It then sends the request to the Identity Insights API through checkSimSwap and prints a clear result to your terminal.

This way, you can test different numbers and time windows interactively without editing the code each time. Later, the same logic could be integrated into a web service or backend system.

func main() {
  loadEnv()

  apiURL := "https://api-eu.vonage.com/v0.1/identity-insights"
  phoneNumber := os.Getenv("PHONE_NUMBER")
  defaultHours := os.Getenv("DEFAULT_HOURS")
  applicationID := os.Getenv("VONAGE_APPLICATION_ID")
  privateKeyPath := os.Getenv("VONAGE_PRIVATE_KEY_PATH")

  creds, err := NewCredentials(applicationID, privateKeyPath)
  if err != nil {
    log.Fatal("Credentials error:", err)
  }
  jwtToken, err := creds.GenerateJWT(15 * time.Minute)
  if err != nil {
    log.Fatal("JWT error:", err)
  }

  fmt.Println("=== Vonage Identity Insights - SIM Swap Checker ===")
  fmt.Printf("Enter phone number [Default: %s]: ", phoneNumber)
  var phone string
  fmt.Scanln(&phone)
  if phone == "" {
    phone = phoneNumber
  }

  fmt.Printf("Enter number of hours to check for SIM swap [Default: %s]: ", defaultHours)
  var hours string
  fmt.Scanln(&hours)
  if hours == "" {
    hours = defaultHours
  }

  checkSimSwap(phone, hours, apiURL, jwtToken)
}

Testing the Script

To run the script, use the following command in your terminal:

go run main.go

Conclusion

With just a few lines of Go and the Vonage Identity Insights API, you can detect SIM swap activity before it turns into a full-scale account takeover. This approach is perfect for testing and can be adapted for production once your application is registered with the Network Registry.

Have a question or something to share? Join the conversation on the Vonage Community Slack, stay up to date with the Developer Newsletter, follow us on X (formerly Twitter), subscribe to our YouTube channel for video tutorials, and follow the Vonage Developer page on LinkedIn, a space for developers to learn and connect with the community. Stay connected, share your progress, and keep up with the latest developer news, tips, and events!

Share:

https://a.storyblok.com/f/270183/384x384/e4e7d1452e/benjamin-aronov.png
Benjamin AronovDeveloper Advocate

Benjamin Aronov is a developer advocate at Vonage. He is a proven community builder with a background in Ruby on Rails. Benjamin enjoys the beaches of Tel Aviv which he calls home. His Tel Aviv base allows him to meet and learn from some of the world's best startup founders. Outside of tech, Benjamin loves traveling the world in search of the perfect pain au chocolat.