diff --git a/README.md b/README.md index fa387b9e..1b02dbd7 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Quick Start Guide 👉 [https://docs.httpsms.com](https://docs.httpsms.com) - [Webhook](#webhook) - [Back Pressure](#back-pressure) - [Message Expiration](#message-expiration) + - [International Phone Number Support](#international-phone-number-support) - [API Clients](#api-clients) - [Flows](#flows) - [Sending an SMS Message](#sending-an-sms-message) @@ -112,6 +113,24 @@ Sometimes it happens that the phone doesn't get the push notification in time an possible to set a timeout for which a message is valid and if a message becomes expired after the timeout elapses, you will be notified. +### International Phone Number Support + +httpSMS supports phone numbers from multiple countries and regions. Phone numbers can be provided in the following formats: + +- **International E.164 format** (recommended): `+33612345678`, `+18005550100`, `+237671234567` +- **National format**: `0612345678` (France), `0123456789` (France) +- **Without leading zeros**: Numbers without country code prefix will be automatically detected if they match common formats + +The system automatically converts phone numbers to the standard E.164 format for consistent handling across different regions. + +**Supported countries include:** +- 🇫🇷 France (e.g., `+33612345678` or `0612345678`) +- 🇺🇸 United States (e.g., `+18005550100`) +- 🇬🇧 United Kingdom (e.g., `+441234567890`) +- 🇩🇪 Germany, 🇪🇸 Spain, 🇮🇹 Italy +- 🇨🇲 Cameroon, 🇳🇬 Nigeria, 🇿🇦 South Africa +- And many more... + ## API Clients - [x] Go: https://github.com/NdoleStudio/httpsms-go diff --git a/api/pkg/requests/request.go b/api/pkg/requests/request.go index 851137d1..a9c34362 100644 --- a/api/pkg/requests/request.go +++ b/api/pkg/requests/request.go @@ -11,6 +11,10 @@ import ( "github.com/nyaruka/phonenumbers" ) +// commonRegionCodes are the region codes tried when parsing phone numbers in national format +// These regions are attempted in order when UNKNOWN_REGION parsing fails +var commonRegionCodes = []string{"FR", "US", "GB", "DE", "ES", "IT", "CM", "NG", "ZA"} + type request struct{} func (input *request) sanitizeAddresses(value []string) []string { @@ -23,12 +27,29 @@ func (input *request) sanitizeAddresses(value []string) []string { func (input *request) sanitizeAddress(value string) string { value = strings.TrimSpace(value) - if !strings.HasPrefix(value, "+") && input.isDigits(value) && len(value) > 9 { - value = "+" + value - } - + + // Try parsing with UNKNOWN_REGION first (works for international format) if number, err := phonenumbers.Parse(value, phonenumbers.UNKNOWN_REGION); err == nil { value = phonenumbers.Format(number, phonenumbers.E164) + return value + } + + // If value doesn't start with + and has more than 9 digits, try adding + + if !strings.HasPrefix(value, "+") && input.isDigits(value) && len(value) > 9 && !strings.HasPrefix(value, "0") { + testValue := "+" + value + if number, err := phonenumbers.Parse(testValue, phonenumbers.UNKNOWN_REGION); err == nil { + value = phonenumbers.Format(number, phonenumbers.E164) + return value + } + } + + // If UNKNOWN_REGION fails, try common region codes for national formats + // This supports phone numbers without country code (e.g., French "0612345678") + for _, region := range commonRegionCodes { + if number, err := phonenumbers.Parse(value, region); err == nil && phonenumbers.IsValidNumber(number) { + value = phonenumbers.Format(number, phonenumbers.E164) + break + } } return value diff --git a/api/pkg/validators/validator.go b/api/pkg/validators/validator.go index bc7111e8..57da57fd 100644 --- a/api/pkg/validators/validator.go +++ b/api/pkg/validators/validator.go @@ -29,12 +29,12 @@ func init() { govalidator.AddCustomRule(phoneNumberRule, func(field string, rule string, message string, value interface{}) error { phoneNumber, ok := value.(string) if !ok { - return fmt.Errorf("The %s field must be a valid E.164 phone number in the international format e.g +18005550100", field) + return fmt.Errorf("The %s field must be a valid E.164 phone number in the international format e.g +18005550100 or +33612345678", field) } _, err := phonenumbers.Parse(phoneNumber, phonenumbers.UNKNOWN_REGION) if err != nil { - return fmt.Errorf("The %s field must be a valid E.164 phone number in the international format e.g +18005550100", field) + return fmt.Errorf("The %s field must be a valid E.164 phone number in the international format e.g +18005550100 or +33612345678", field) } return nil @@ -49,7 +49,7 @@ func init() { for index, number := range phoneNumbers { _, err := phonenumbers.Parse(number, phonenumbers.UNKNOWN_REGION) if err != nil { - return fmt.Errorf("The %s field in index [%d] must be a valid E.164 phone number in the international format e.g +18005550100", field, index) + return fmt.Errorf("The %s field in index [%d] must be a valid E.164 phone number in the international format e.g +18005550100 or +33612345678", field, index) } }