Signing requests

Signatures validate the authenticity of the person who interacts with Nexmo.

You use a signature to:

A signature is an MD5 hash  of:

The signature has a leading &. All parameters in the hash input, apart from your SIGNATURE_SECRET are separated by &.

HMAC-SHA1/256/512   is also supported. Contact support@nexmo.com for more information.

Note: Using signatures is an optional improvement on using the standard api_secret. You use the SIGNATURE_SECRET instead of your api_secret in a signed request.

The following example shows a signed request to the SMS API:

https://rest.nexmo.com/sms/xml?api_key=API_KEY&from=Nexmo&to=447700900000&type=text&text=Hello+from+Nexmo&status-report-req=false&timestamp=1461605396&sig=SIGNATURE

The workflow for using signed messages is:

#{alt_text}

  1. Create a signed request to send an SMS.
  2. Check the response codes and ensure that you sent the request correctly.
  3. Your message is delivered to the handset. The user's handset returns a delivery receipt.
  4. If you requested signed delivery receipts and inbound messages validate the signature.

Implementing the message signing workflow

When you create a Nexmo account you will be provided a signature secret. These can be found in your account settings  in the Nexmo Dashboard.

To sign your messages:

Create a signed request:

var https = require('https');
var crypto = require('crypto');

var security_secret = 'SECURITY_SECRET';
var security_method = 'sha256' // Possible values md5, sha1, sha256 or sha512

var parameters = {
 api_key: 'API_KEY',
 to: '441632960960',
 from: '441632960961',
 text: 'Hello from Nexmo',
 type: 'text',
 timestamp: Math.floor(new Date() / 1000)
};

//Sort the parameters
var param_array = new Array();
for (key in parameters) {
    param_array.push(key + '=' + parameters[key]);
}

var sorted_params = param_array.sort();

if (security_method == 'md5') {
    var signing_url = '&' + sorted_params.join('&') + security_secret ;
    var hash = crypto.createHash(security_method).update(signing_url).digest('hex');
} else {
    var signing_url = '&' + sorted_params.join('&');
    var hash = crypto.createHmac(security_method, security_secret).update(signing_url).digest('hex');
}

parameters['sig'] = hash ;
var data = JSON.stringify(parameters );

var options = {
 host: 'rest.nexmo.com',
 path: '/sms/json',
 port: 443,
 method: 'POST',
 headers: {
   'Content-Type': 'application/json',
   'Content-Length': Buffer.byteLength(data)
 }
};

var req = https.request(options);

req.write(data);
req.end();

var responseData = '';
req.on('response', function(res){
 res.on('data', function(chunk){
   responseData += chunk;
 });
 res.on('end', function(){
   console.log(JSON.parse(responseData));
 });
});
<?php

$base_url = 'https://rest.nexmo.com/sms/json?';
$security_secret = 'SECURITY_SECRET';

//The timestamps used in the signature are in UTC + 0
date_default_timezone_set('UTC');

$params = [
      'api_key' =>  'API_KEY',
      'to' => '441632960960',
      'from' => '441632960061',
      'text' => 'Hello from Nexmo',
      'type' => 'text',
      'timestamp' => time() - date('Z')
    ];

//sort your parameters
ksort($params);

//create base string
$signing_url = '&' . urldecode(http_build_query($params)) . $security_secret;

//Add your md5 hash of your parameters to your parameters
$params['sig'] = md5($signing_url);

//Create your request URL
$url = $base_url . http_build_query($params);

//Run your request
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

echo $response;
import urllib
import urllib2
import time
import md5
import collections
import json
from datetime import datetime
import calendar

base_url = 'https://rest.nexmo.com/sms/json?'
security_secret = 'SECURITY_SECRET'

# The timestamps used in the signature are in UTC
d = datetime.utcnow()

params = {
    'api_key': 'API_KEY',
    'to': '441632960960',
    'from': '441632960961',
    'text': 'Hello from Nexmo',
    'type': 'text',
    'timestamp': calendar.timegm(d.utctimetuple())
}

# Sort your parameters
sortedparams = collections.OrderedDict(sorted(params.items()))
signing_url = '&' + urllib.unquote_plus(urllib.urlencode(sortedparams)) + security_secret

# Add your md5 hash of your parameters to your parameters
m = md5.new()
m.update(signing_url)
params['sig'] = m.hexdigest()

# Create the request
url = base_url + urllib.urlencode(params)
request = urllib2.Request(url)
request.add_header('Accept', 'application/json')

# Make the request to Nexmo
response = urllib2.urlopen(request)
require 'net/http'
require 'uri'
require 'digest'
require 'cgi'

# Create your parameters
base_url = 'https://rest.nexmo.com/sms/json'
security_secret = 'SECURITY_SECRET'

uri = URI.parse(base_url)
params = {
    'api_key' => 'API_KEY',
    'to' => '441632960960',
    'from' => '441632960961',
    'text' => 'Hello from Nexmo'
    'type' => 'text',
    'timestamp' => Time.now.getutc.to_i
}

# Add your md5 hash of your sorted parameters to your parameters
signing_url = '&' + CGI::unescape(URI.encode_www_form(params.sort)) + security_secret
params['sig'] = Digest::MD5.hexdigest signing_url

# Make the request to Nexmo
response = Net::HTTP.post_form(uri, params)

puts response.body

Check the response codes to ensure that you sent the request to Nexmo correctly:

//Decode the json object you retrieved when you ran the request.

var decodedResponse = JSON.parse(responseData);

console.log('You sent ' + decodedResponse['message-count'] + ' messages.\n');

decodedResponse['messages'].forEach(function(message) {
    if (message['status'] === "0") {
      console.log('Success ' + decodedResponse['message-id']);
    }
    else {
      console.log('Error ' + decodedResponse['status']  + ' ' +  decodedResponse['error-text']);
    }
});
<?php

$base_url = 'https://rest.nexmo.com/sms/json?';
$security_secret = 'SECURITY_SECRET';

//The timestamps used in the signature are in UTC + 0
date_default_timezone_set('UTC');

$params = [
      'api_key' =>  'API_KEY',
      'to' => '441632960960',
      'from' => '441632960061',
      'text' => 'Hello from Nexmo',
      'type' => 'text',
      'timestamp' => time() - date('Z')
    ];

//sort your parameters
ksort($params);

//create base string
$signing_url = '&' . urldecode(http_build_query($params)) . $security_secret;

//Add your md5 hash of your parameters to your parameters
$params['sig'] = md5($signing_url);

//Create your request URL
$url = $base_url . http_build_query($params);

//Run your request
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

echo $response;

import json

# Using the response object from the request

if response.code == 200 :
    data = response.read()
    # Decode JSON response from UTF-8
    decoded_response = json.loads(data.decode('utf-8'))
    # Check if your messages are successful
    messages = decoded_response["messages"]
    for message in messages:
        if message["status"] == "0":
            print "success"
else :
    # Check the errors
    print "Unexpected HTTP {code} response from nexmo api". response.code
require 'json'

#Decode the json object from the response object you retrieved from the request.
if response.kind_of? Net::HTTPOK
  decoded_response = JSON.parse(response.body )

  messagecount = decoded_response["message-count"]

  decoded_response["messages"].each do |message|
    if message["status"] == "0"
        p "message " + message["message-id"] + " sent successfully.\n"
      else
        p "message has error " + message["status"]  + " " + message["error-text"]
    end
  end
else
  puts response.code + " error sending message"
end

If you did not generate the signature correctly the status is 14, invalid signature

Your message is delivered to the handset. The user's handset returns a delivery receipt.

If your delivery receipts and inbound messages are signed, validate the signature:

var http = require('http')
var url = require('url')
var crypto = require('crypto');

var security_secret = 'SECURITY_SECRET';
// create the http server
http.createServer(function (request, response) {

  if(request.method=='POST') {
      //Do something for post request
      console.log("post message");
  }
  else if(request.method=='GET') {
      //Turn the query string onto an object
      var url_parts = url.parse(request.url,true).query;
      if (url_parts.hasOwnProperty('sig')){

        //Compare the local time with the timestamp
        var now = Math.floor(new Date() / 1000);
        var message_timestamp = url_parts.timestamp;

        //Message cannot be more than 5 minutes old
        var max_delta = 5 * 60;
        difference = Math.abs( now - message_timestamp );
        if (difference > max_delta)
            console.log("Timestamp difference greater than 5 minutes");
        else {
            //Sort the parameters
            message_signature = url_parts.sig;
            //Remove the signature from the request parameters
            delete url_parts.sig;

            //Create the signing url using the sorted parameters and your SECURITY_SECRET
            var param_array = new Array();
            for (key in url_parts) {
                param_array.push(key + '=' + unescape(url_parts[key]));
            }
            var sorted_params = param_array.sort();
            var signing_url = '&' + sorted_params.join('&') + security_secret ;

            //Add your md5 hash of your parameters to your parameters
            var generated_signature = crypto.createHash('md5').update(signing_url).digest('hex');
            //A timing attack safe string comparison to validate hash
            var valid = 0;
            for (var i = 0; i < generated_signature.length; ++i) {
                valid |= (generated_signature.charCodeAt(i) ^ message_signature.charCodeAt(i));
            }

            if (valid == 0)
              console.log("Message was sent by Nexmo");
            else
              console.log("Alert: message not sent by Nexmo!");
        }

      }
  }
  //Send the 200 ok to Nexmo so you don't get sent the DLR again.
  response.writeHead(200, {"Content-Type": "text/html"});
  response.write("hello iain");
  response.end();

}).listen(80);
<?php

// work with get or post
$request = array_merge($_GET, $_POST);
$security_secret = 'SECURITY_SECRET';

//If hash_equals is not supported by your version of PHP
//Here is a timing attack safe string comparison
if(!function_exists('hash_equals')) {
    function hash_equals($a, $b) {
        return substr_count($a ^ $b, "\0") * 2 === strlen($a . $b);
    }
}

//If the request has been signed
if(isset($request['sig'])){
  //The timestamps used in the signature are in UTC + 0
  //Create a UTC timestamp for now and compare
  $now = time() - date('Z');
  $message_timestamp = strtotime($request['message-timestamp']);
  $difference = abs ($now - $message_timestamp);

  //Message cannot be more than 5 minutes old
  $max_delta = 5 * 60;
  if ($difference > $max_delta)
      error_log("Timestamp difference greater than 5 minutes");
  else {
    //Store the signature locally and remove from the params
    $message_signature = $request['sig'];
    unset($request['sig']);
    // Sort the parameters so they are in alphabetic order
    ksort($request);
    // Generate a signature from the parameters plus your security secret
    $generated_signature = md5('&' .urldecode(http_build_query($request)) .$security_secret);
    // A timing attack safe string comparison to validate the signatures
    if (hash_equals($message_signature, $generated_signature))
        error_log("Message was sent by Nexmo");
    else
        error_log("Alert: message not sent by Nexmo!");
  }
}
# To run this code, replace the MyHandler in
# https://wiki.python.org/moin/BaseHttpServer With the following code,
import urllib
import time
import BaseHTTPServer
import re
import json
from urlparse import urlparse, parse_qs
from datetime import datetime
import datetime
import collections
import md5
import calendar

security_secret = 'SECURITY_SECRET'

class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(s):
        """Tell Nexmo that you have received the GET request."""
        s.send_response(200)
        s.send_header("Content-type", "text/html")
        s.end_headers()
        """Parse parameters in the GET request"""
        parsed_path = urlparse(s.path)
        try:
                callback = dict(
                [p.split('=') for p in parsed_path[4].split('&')])
        except:
                callback = {}

        # Check if the callback is signed
        if 'sig' in callback:
            # Message cannot be more than 5 minutes old
            # The timestamps used in the signature are in UTC
            # Create a UTC timestamp for now and compare
            d = datetime.utcnow()
            now = calendar.timegm(d.utctimetuple())
            message_timestamp = int( callback['timestamp'])
            max_delta = 5 * 60
            difference = abs(now - message_timestamp)
            if (difference >  max_delta ):
                print("Timestamp difference greater than 5 minutes")
            else:
                # Remove the signature from the request parameters
                message_signature = callback['sig']
                del callback['sig']
                # Sort the parameters into alphabetic order
                sortedparams = collections.OrderedDict(sorted(callback.items()))
                # Remove the encoding from the timestamp, then put it back in the params
                tempstamp = sortedparams['message-timestamp']
                tempstamp = urllib.unquote_plus(tempstamp).decode('utf8')
                sortedparams['message-timestamp'] = tempstamp
                # Generate a signature from the parameters and your security secret
                encoded_params = urllib.urlencode(sortedparams)
                signing_url = '&' + urllib.unquote_plus(encoded_params).decode('utf8')
                signing_url += security_secret
                m = md5.new()
                m.update(signing_url)
                generated_signature = m.hexdigest()
                # Validate that the signatures match
                if ( constant_time_compare (message_signature, generated_signature)):
                    print("Message was sent by Nexmo")
                else :
                    print("Alert: message not sent by Nexmo!")

        else:
            print "This callback has not been signed"

# A timing attack safe string comparison
def constant_time_compare(val1, val2):
    """
    Returns True if the two strings are equal.
    This function executes in constant time only when the two strings have the same length
    """
    if len(val1) != len(val2):
        return False
    result = 0
    for x, y in zip(val1, val2):
        result |= ord(x) ^ ord(y)
    return result == 0
require 'socket'
require "net/http"
require "uri"
require 'digest'
require 'cgi'

def handle_delivery_receipt(request_line, security_secret)
#Parse the parameters and check if the message was delivered

  params = URI::decode_www_form(request_line).to_h
  if ! params["sig"].nil?
    now = Time.now.getutc.to_i
    message_timestamp = params['timestamp'].to_i
    max_delta = 5 * 60
    difference =  now - message_timestamp
    if ( difference.abs >  max_delta )
        p("Timestamp difference greater than 5 minutes")
    else
      #Remove the signature from the request parameters
      message_signature = params['sig']
      params['msisdn'] = params['/?msisdn']
      params.delete('sig')
      params.delete('/?msisdn')

      # Create the signing url using the sorted parameters and your SECURITY_SECRET
      signing_url = '&' + CGI::unescape(URI.encode_www_form(params.sort)) + security_secret
      #Add your md5 hash of your parameters to your parameters
      generated_signature = Digest::MD5.hexdigest(signing_url)

      if (secure_compare( message_signature, generated_signature))
        p("Message was sent by Nexmo");
      else
        print("Alert: message not sent by Nexmo!")
      end
    end
  end
end

# A timing attack safe string comparison
def secure_compare(a, b)
     return false if a.empty? || b.empty? || a.bytesize != b.bytesize
     l = a.unpack "C#{a.bytesize}"

     res = 0
     b.each_byte { |byte| res |= byte ^ l.shift }
     res == 0
   end

security_secret = 'SECURITY_SECRET'
# Initialize a TCPServer
server = TCPServer.new('', 9999)

# Wait for connections
loop do
  # Wait until a client connects
  socket = server.accept

  method, path = socket.gets.split
  #Check the signature
  handle_delivery_receipt(path, security_secret)

  # Return the 200 so Nexmo does not send the DLR to you repeatedly
  resp = "Thank you"
  headers = ["HTTP/1.1 200 OK",
             "Content-Type: text/html; charset=iso-8859-1",
             "Content-Length: #{resp.length}\r\n\r\n"].join("\r\n")
  socket.puts headers
  socket.puts resp
  # Close the socket, terminating the connection
  socket.close
end

Troubleshooting signatures

Here are some tips and pitfalls to look out for when working with signed messages.

Check the response for details

If the message isn't sent as expected, check the response for any error codes that were returned. This will usually give you more detail on what to do next.

Error 14: Invalid Signature

If the text being sent includes any special characters such as & (ampersand) or = (equals), then these need to be replaced in the text used to create the signature.

A general approach would be:

The original text can be still be sent/received, the character replacements are only needed to generate the signature.