Mikko Kortelainen

Sonera CStream Messaging Web Service API with Python and SUDS

In this article I will show you how to use the Sonera CStream Messaging Web Service API to send an SMS using Python, and a library called  SUDS. The CStream API is two-way service for both sending and receiving messages. You obviously need to pay for the service to get access. After you have your credentials, you can start using the service.

The SUDS is a lightweight SOAP Python client for exploring and using web services. A recent version can be installed on Debian based distros with "sudo apt-get install python-suds", or on almost anything with "pip install suds".

The one thing good to know before starting out, is that the Sonera web service uses WS Security for authentication, which is found in the suds.wsse module.

Let's start hacking in iPython:

kortsi@localhost:~$ ipython
Python 2.7.3 (default, Sep 26 2012, 21:51:14)
Type "copyright", "credits" or "license" for more information.

IPython 0.13.1.rc2 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
          -> Python's own help system.
object    -> Details about 'object', use 'object??' for extra details.

In [1]: import logging

In [2]: logging.basicConfig(level=logging.DEBUG)

In [3]: logging.getLogger('suds.client').setLevel(logging.DEBUG)

In [4]: from suds.client import Client

In [5]: client = Client('https://saarni.front.sonera.fi/ws/messaging-v2.wsdl')

At this point you should see a lot of debug messages on the screen. Thats the SUDS querying the WSDL schema and finding out what's possible. You can take a look at it if it you are interested, but there is a better way to explore the metadata.

In [6]: print client

Suds ( https://fedorahosted.org/suds/ )  version: 0.4.1 (beta)  build: R703-20101015

Service ( messaging-v2Service ) tns="http://www.lekab.com/schema/messaging-v2/messages"
   Prefixes (1)
      ns0 = "http://www.lekab.com/schema/messaging-v2/messages"
   Ports (1):
      (messaging-v2Soap11)
         Methods (3):
            GetIncomingMessages(MessageIds messageIds, Attributes attributes, )
            GetMessageStatus(MessageIds messageIds, Attributes attributes, )
            Send(xs:string sender, Recipients recipients, xs:boolean replyable, xs:string conversationId, Priority priority, xs:dateTime scheduledDelivery, xs:dateTime validTo, xs:string tariff, xs:string contentCategory, xs:string contentMetaData, xs:double vat, xs:string referenceId, Data data, Attributes attributes, )
         Types (21):
            Attachment
            Attachments
            Attribute
            Attributes
            Data
            ErrorDetail
            ErrorDetails
            IncomingMessage
            IncomingMessages
            MessageIds
            MessageStatus
            MessageStatuses
            MmsData
            MmsPayload
            OutgoingMessage
            Payload
            Priority
            Recipients
            SmsData
            SmsPayload
            Value

The list of available methods and and data types is what we are interested in.

The three methods there match the API specification. The names are pretty much self-explanatory:

  • Send() sends text messages
  • GetMessageStatus() queries the status of sent messages
  • GetIncomingMessages() retrieves incoming messages

To invoke those methods, we must be able to create data in specified types. To send an SMS, we need to create an SmsData object, wrapped in a Data object. We can have SUDS create them for us:

In [7]: data = client.factory.create('Data')

In [8]: smsdata = client.factory.create('SmsData')

In [9]: data.sms = smsdata

In [10]: print data
(Data){
   sms =
      (SmsData){
         payload =
            (SmsPayload){
               message = None
               udh = None
            }
         encoding = None
         replaceIfPresent = None
         flash = None
         port = None
         attributes =
            (Attributes){
               attribute[] = <empty>
            }
      }
 }

That shows what the Data should look like. Let's also create an SmsPayload object:

In [11]: payload = client.factory.create('SmsPayload')

In [12]: print payload
(SmsPayload){
   message = None
   udh = None
 }

According to the specification, the message attribute must be a "base64 encoded UTF-8 encoded string". Let's test with a string that requires more than plain ASCII:

In [13]: testmessage = u'Tämä on testi! ("This is a test!" in Finnish)'

In [14]: import base64

In [15]: payload.message = base64.encodestring(testmessage.encode('utf8')).strip()

In [16]: print payload
(SmsPayload){
   message = "VMOkbcOkIG9uIHRlc3RpISAoIlRoaXMgaXMgYSB0ZXN0ISIgaW4gRmlubmlzaCk="
   udh = None
 }

In [17]: smsdata.payload = payload

In [18]: print data
(Data){
   sms =
      (SmsData){
         payload =
            (SmsPayload){
               message = "VMOkbcOkIG9uIHRlc3RpISAoIlRoaXMgaXMgYSB0ZXN0ISIgaW4gRmlubmlzaCk="
               udh = None
            }
         encoding = None
         replaceIfPresent = None
         flash = None
         port = None
         attributes =
            (Attributes){
               attribute[] = <empty>
            }
      }
 }

Now we have a payload set. The Data object is now ready for sending. We still need to create a list of recipients:

In [19]: recipients = client.factory.create('Recipients')

In [20]: recipients.recipient.append('358001234567')

In [21]: print recipients
(Recipients){
   recipient[] =
      "358001234567",
 }

The recipient number must be a string, and it must have a country code. A sender id or number is also required, but it can be whatever you choose. We can give it directly to the Send() method when we send the message. But before that, we must set up authentication:

In [22]: from suds.wsse import Security, UsernameToken, Timestamp

In [23]: security = Security()

In [24]: username = UsernameToken('username', 'secretpassword')

In [25]: security.tokens.append(username)

In [26]: timestamp = Timestamp(validity=120)

In [27]: security.tokens.append(timestamp)

In [28]: client.set_options(wsse=security)

We set up a username/password token, and a timestamp. Now we have 120 seconds of time to send a message using the security tokens.

At this point, we should have everything we need to send our message. Use the Send() method:

In [29]: client.service.Send(sender='SENDERID', recipients=recipients, data=data)

You will get a reply from the remote server. It will either tell you that it failed for some reason, but if everything went ok, the message was queued for delivery, and the response object will look something like this:

Out[29]:
(reply){
   messageStatus[] =
      (MessageStatus){
         statusCode = 0
         statusText = "QUEUED"
         id = "1-9876543"
         sender = "SENDERID"
         recipient = "358001234567"
         time = 2013-03-11 15:52:37.000984
         billingStatus = 0
         attributes =
            (Attributes){
               attribute[] =
                  (Attribute){
                     name = "NumberOfMessages"
                     value =
                        (Value){
                           integer = 1
                        }
                  },
                  (Attribute){
                     name = "NumberOfCharacters"
                     value =
                        (Value){
                           integer = 45
                        }
                  },
            }
      },
   attributes =
      (Attributes){
         attribute[] =
            (Attribute){
               name = "TotalNumberOfMessages"
               value =
                  (Value){
                     integer = 1
                  }
            },
      }
 }

The recipient should receive the message shortly.

To wrap it up, let's make a simple class out of it. Put this to a file called cstream.py:

WSDL_URL = 'https://saarni.front.sonera.fi/ws/messaging-v2.wsdl'

from base64 import encodestring as b64enc
from suds.client import Client
from suds.wsse import Security, UsernameToken, Timestamp

class SimpleSmsSender:
  """Sends an SMS using CStream Web Service API

Give your credentials as arguments to the constructor. Example:

  sender = SimpleSmsSender('username', 'secretpassword')
  """

  def __init__(self, username, password):
    self.username = username
    self.password = password
    self.client = Client(WSDL_URL)

  def send(self, sender, recipient, message):
    """Give sender id, recipient number and message as strings. The message
can be a unicode string. Example:

  sender.send('SENDERID', '358001234567', u'Hello world!')
    """
    f = self.client.factory
    data = f.create('Data')
    smsdata = f.create('SmsData')
    data.sms = smsdata
    payload = f.create('SmsPayload')
    payload.message = b64enc(message.encode('utf8')).strip()
    smsdata.payload = payload
    recipients = f.create('Recipients')
    recipients.recipient.append(recipient)
    security = Security()
    username = UsernameToken(self.username, self.password)
    security.tokens.append(username)
    timestamp = Timestamp()
    security.tokens.append(timestamp)
    self.client.set_options(wsse=security)
    return self.client.service.Send(sender=sender,
                                    recipients=recipients,
                                    data=data)

To use:

In [1]: from cstream import SimpleSmsSender

In [2]: sender = SimpleSmsSender('username', 'secretpassword')

In [3]: sender.send('FROM ME', '358001234567', u'Hello world')

That's it! The receiving of messages, the checking of message delivery status, and the handling of exceptions are left as exercises to the reader (and to myself).