First a little background. I have several projects that need to send SMS texts for various notifications. I use a service called Twilio to actually send those SMS messages. I was (and still am) unable to figure out how to format a POST request to fire off directly to Twilio, so I use an intermediate service from Script.io
I need to eliminate the use of Scriptr.io and issue these Post requests directly to Twilio. The code (json?) to fire these requests in Scriptr looks like:
var callResult = http.request({
"url" : "https://api.twilio.com/2010-04-01/Accounts/xxx...mytoken...xxx/Messages.json",
"method": "POST",
"params": {"From": "2055551212", "To": “6095551212”, "Body": “Text I want to send”},
"authCreds": ["xxx...mytoken...xxx "]
});
[NOTE: I’ve simplified some things – the phone numbers, text, etc are actually done using some variables – I’ve hard-coded the above just to make it clearer what this is doing.]
My question is how to do this directly from Arduinos (MKR1000, etc).
I’m able to send a POST request to Scriptr from Arduino using the following code. What I don’t know how to do is format the above POST request to get it to work correctly as part of the client.print call below.
// Error checking and retry logic removed for clarity
const char* bearer = "xxx...scriptr token...xxx";
WiFi.begin(ssid, password);
POSTtoScriptr (“Text I want to send”)
...
int POSTtoScriptr (String msg) {
if (!client.connect("api.scriptrapps.io", 443)) {
return false;
}
client.print(String("POST ") + "/SMS/?sms=" + msg + " HTTP/1.1\r\n" +
"Host: api.scriptrapps.io\r\n" +
"Authorization:bearer " + bearer + "\r\n" + // 23 + 68
"Content-type: application/x-www-form-urlencoded\r\n" +
"Connection: close\r\n\r\n");
unsigned long timeout = millis();
while (client.available() == 0) {
if (millis() - timeout > 5000) {
client.stop();
return false;
}
yield();
}
while(client.available()){
String line = client.readStringUntil('\r');
yield();
}
return true;
}
To send an SMS directly from an Arduino (e.g., MKR1000) to Twilio without using Scriptr.io, you need to format the HTTP POST request correctly. The key steps involve setting up the HTTP headers, including the Authorization header for authentication, and the body of the request with the message details. Here's how you can adapt your existing code to send a POST request directly to Twilio:
Establish a WiFi connection: Ensure your Arduino is connected to the internet.
Prepare the HTTP POST request: This includes setting the correct URL, headers, and body content.
Send the request: Use the client.print method to send the request to Twilio's API.
Here's an example code snippet based on your requirements:
#include <SPI.h>
#include <WiFiNINA.h>
// Replace with your network credentials
const char* ssid = "your_SSID";
const char* password = "your_PASSWORD";
// Twilio credentials
const char* accountSid = "your_TWILIO_ACCOUNT_SID";
const char* authToken = "your_TWILIO_AUTH_TOKEN";
const char* fromNumber = "your_TWILIO_PHONE_NUMBER";
const char* toNumber = "RECIPIENT_PHONE_NUMBER";
// The message you want to send
const char* messageBody = "Text I want to send";
// Initialize the WiFi client library
WiFiClient client;
void setup() {
// Start the serial communication
Serial.begin(9600);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
// Send the POST request to Twilio
sendSMS(toNumber, messageBody);
}
void loop() {
// Nothing to do here
}
void sendSMS(const char* to, const char* body) {
if (!client.connect("api.twilio.com", 443)) {
Serial.println("Connection to Twilio failed");
return;
}
// Prepare the HTTP POST request
String authString = "Basic " + String(accountSid) + ":" + String(authToken);
String postData = "From=" + String(fromNumber) + "&To=" + String(to) + "&Body=" + String(body);
int postDataLength = postData.length();
// Send the request
client.println("POST /2010-04-01/Accounts/" + String(accountSid) + "/Messages.json HTTP/1.1");
client.println("Host: api.twilio.com");
client.println("Authorization: " + authString);
client.println("Content-Type: application/x-www-form-urlencoded");
client.print("Content-Length: ");
client.println(postDataLength);
client.println();
client.println(postData);
// Wait for the response
while (client.connected()) {
if (client.available()) {
String line = client.readStringUntil('\r');
Serial.print(line);
}
}
client.stop();
}
This code snippet demonstrates how to send an SMS directly from an Arduino to Twilio. Make sure to replace placeholders like your_SSID, your_PASSWORD, your_TWILIO_ACCOUNT_SID, your_TWILIO_AUTH_TOKEN, your_TWILIO_PHONE_NUMBER, and RECIPIENT_PHONE_NUMBER with your actual credentials and phone numbers.
Remember, the authString is constructed by encoding your Twilio Account SID and Auth Token in Base64 format. This is a crucial step for authentication with Twilio's API. The postData string contains the message details, including the sender's number (From), the recipient's number (To), and the message body (Body). The Content-Length header is dynamically set to the length of the postData string.
This approach eliminates the need for Scriptr.io and allows you to send SMS messages directly from your Arduino to Twilio.
The encoding for Basic auth bakes in the colon, effectively Basic base64(name:password)
not Basic base64(name):base64(password)
as implied by the code above for authString
The postData for a body of type application/x-www-form-urlencoded must encode spaces and many "reserved" punctuation characters, like = and ?
With the comma outside the encoded information. Wouldn't this mean that the colon is not to be encoded, or does sending as json change that?
Also, do I need to append \r\n or does the .println take care of that?
And thanks for pointing out that many punctuation characters need to be encoded - I was aware of that but anyone reading this in the future needs to know.
SIDE NOTE FOR ANYONE LOOKING AT THIS IN THE FUTURE:
In my attempt to simplify the original code, I mis-typed in my original post, The corrected lines should be:
Not being familiar with neither Scriptr nor Twilio, it's pretty clear Scriptr is expecting two strings in an array for the authCreds in this case, and the comma just separates those in the JSON/JavaScript. And then presumably it does the work of concatenating them with the colon and encoding.
.println adds one \r\n. When you're printing one line at a time, use it unless you need to do printf instead. For example, these two are equivalent
POST /2010-04-01/Accounts/AC1b2390c4c50cf4b24670fb88e3fe9a33/Messages.json HTTP/1.1
Host: https://api.twilio.com
Authorization: Basic QU...Base64 Account SID:Token...==
Content-Type: application/x-www-form-urlencoded
Connection: close
Body=Yourmessagehere&From=2055551212&To=6095551212
I've tried just the Token too. The auth string is SID:Token all encoded as a single string.
I must be missing something simple, but I can't find it.
Also, I originally had api.twilio.com and the response indicated that it was transferring to HTTPS so I prefixed the API URL with https:// and the warning went away.
is just for the host name. No protocol nor port. Those are in the URL because that's "where the message should go and how is it sent" versus the content of the message once you get there. This header is required because a web server can at given IP address and port can host multiple web sites. Should be
Host: api.twilio.com
I'd omit the Connection header. Most importantly there must be a blank line between the headers and the body.
If everything else is good but the auth is incorrect (while formed correctly), you should get 401 or 403 instead of 400. For a 400, the server may have more details in its response body.
It fails the same (400 Bad Request) with or without the https:// in Host: api.twilio.com
But when the https:// prefix isn't there, I also get
400 The plain HTTP request was sent to HTTPS port
I removed the Connection header line and also added a blank line before Body=..... and still get the 400 error. There must be something stupid I'm missing but I sure can't see it.
Removing the prefix was actually progress. With it there, the Host header value is malformed, and apparently there is no specific message for that -- maybe because how can you screw up something so basic
With that fixed, you now have an actual "legitimate" error, and it's exactly what that message says. You are connecting on the default HTTPS port, 443. But you are using the plain WiFi/HTTP client, not the secure one. So replace
WiFiClient client;
with
WiFiClientSecure client;
client.setInsecure();
If the client is declared globally, you put the call to setInsecure in your function body, before you use it (just once). You need this call because you don't have the server's or root certificate, to validate the server is who they claim to be. So you'll just trust them for now, but it's something you can fix. The connection is still encrypted. (If you don't make the call, the client will refuse to connect.)
WiFiClientSecure is for ESP32 boards (at least). I am not familiar with the others, but looks like for WiFiSSLClient, there is no option for insecure. You need to Upload SSL Root Certificates. This is under the IDE Tools menu, but as of IDE 2.2.0, MKR 1000 is no longer supported, so you have to follow an older procedure with IDE 1.x.
Typically, a site has its relatively short-term certificate issued by an intermediary; and in turn that intermediary has its certs backed by a long-lived root certificate. You can find this info by visiting the site in a browser, clicking the lock next to the start of the URL, and then poking around the resulting UI. For example, the root for google.com is Google Trust Services "GTS Root R1"; for github.com, "USERTrust ECC Certification Authority"; and for twilio.com, "DigiCert Global Root G2". Multiple sites from different entities can share the same root, so you're not necessarily uploading a root for each site. Apparently the Firmware/Cert Updater will just go take a look and upload it if needed.
The example for WiFiSSLClient, which is also here at Github if it's not in your Examples menu, tries to connect to Google. I don't know if the root for that is already uploaded. You can try swapping with Twilio before and/or after uploading the certs to make sure that is working.
If it's a certificate issue, I can probably obtain it from Twilio. However, if it's a short term certificate does that mean that every so often I'll need to upload a new certificate into the MKR1000s?
I did run across something in the IDE version I'm using (1.8.9) under the firmware updater for adding SSL certificates.
Apparently the native Wifi support in the WiFi101 library does not support SSL very well. I found an article with a similar issue on a different API, and their solution was to use ArduinoBearSSL library.
THAT WORKED! I am now able to fire off requests to Twilio.
Thank you all for all of your help! It was definitely needed to get my POST request formatted correctly.