REST Streaming

The Nest service supports REST streaming for connecting products directly to Nest services and for cloud-to-cloud integrations. Use REST streaming if you want your application to listen for changes to Nest devices. This is different from REST calls, where you need to poll the Nest API server to get changes.

In REST streaming, instead of returning information and then closing the connection, the Nest API keeps the connection open. Any time there is a state change, the Nest API sends the event using the open connection, so you can receive the new state. The Nest API sends a Content-Type: text/event-stream response header with the event name and data for the state change. It also sends a Connection: keep-alive header to indicate to the product to keep the connection open.

REST streaming uses Server-Sent Events (SSE) and the EventSource interface, which is implemented in some languages and browsers. For more details, see https://www.html5rocks.com/en/tutorials/eventsource/basics/.

The important thing to know about REST streaming is that the TCP connection remains open for a single user. In other words, REST streaming allows for only one token (one user) per socket. Thousands of users means thousands of open sockets.

Prerequisites

Before making API calls, obtain an access token, as described in Authenticating with OAuth 2.0 and Sample Code for Authorization.

How to make a REST streaming call

  1. Use the root Nest API endpoint https://developer-api.nest.com (or a more granular endpoint) with a request header to accept a content type of text/event-stream instead of application/json. See API Read Examples for a comparison.
  2. Include the access token. We recommend you comply with this OAuth standard, which offers increased security by using the "Bearer" authentication scheme to transmit the access token. Calls with client credentials in the URL are not recommended.
  3. Listen for events such as open, put, auth_revoked, and error. Note that the patch event is not supported in the Nest API.

Examples

In the following examples, replace YOUR_TOKEN_HERE with your specific access token, such as "c.twC2q...".

Subscribe to changes in structures and devices

To get a response when changes occur in structure and device data (base level), subscribe to the root-level URL. For more granular responses, modify the URL as needed.

Curl
curl -v --location-trusted https://developer-api.nest.com/ \
  -H "Accept:text/event-stream" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "Cache-Control: no-cache" -v -L \
Go
package main

import (
    "bufio"
    "bytes"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
)

const (
    token = "YOUR_TOKEN_HERE" // Update with your token
    url = "https://developer-api.nest.com/"
    eventPrefx = "event:"
    dataPrefx = "data:"
)

var (
    eventPrefxBytes = []byte(eventPrefx)
    dataPrefxBytes = []byte(dataPrefx)
)

type serverSentEvent struct {
    name string
    data string
}

func main() {
    resp, _ := getResponse(url, token)
    defer resp.Body.Close()
    ssevents := getServerSentEvents(*resp)
    for serverSentEvent := range ssevents {
        switch(serverSentEvent.name) {
            case "put":
                fmt.Println("Got updates from the Nest API: ")
            case "keep-alive":
                fmt.Println("No updates, just receiving event to keep the connection open.")
            case "auth_revoked":
                fmt.Println("Revoked token: ")
            case "error":
                fmt.Println("Got Error message: ")
            default:
                 fmt.Println("Unknown event, no handler for it: ")
        }
        fmt.Println(eventPrefx, serverSentEvent.name)
        if (serverSentEvent.data != "null") {
            fmt.Println(dataPrefx, serverSentEvent.data, "\n")
        }
    }
}

func getServerSentEvents(resp http.Response)(chan serverSentEvent) {
    events := make(chan serverSentEvent)
    go func() {
        event := serverSentEvent{}
        reader := bufio.NewReader(resp.Body)
        for {
            line, err := reader.ReadBytes('\n')
            if err != nil && err != io.EOF {
                fmt.Fprintf(os.Stderr, "Got an error reading response: %s\n", err)
                break
            }
            switch {
                case bytes.HasPrefix(line, eventPrefxBytes):
                    event.name = getEventName(line)

                case bytes.HasPrefix(line, dataPrefxBytes):
                    event.data = getEventData(line)
                    events <- event
                    event = serverSentEvent{}
            }
            // Receive EOF (end of file) on the response if auth token revoked or other errors.
            // Checking for it here so it can return the "auth_revoked" event above if it occurs.
            if err == io.EOF {
                fmt.Fprintf(os.Stderr, "Got EOF reading response: %s\n", err)
                break
            }
         }
         close(events)
    }()
    return events
}

func authorizationValue(token string) (bearer string) {
    return fmt.Sprintf("Bearer %s", token)
}

func getEventName(line []byte) (eventName string) {
    return string(line[len(eventPrefx)+1 : len(line)-1])
}

func getEventData(line []byte) (eventType string) {
    return string(line[len(dataPrefx)+1 : len(line)-1])
}

func getResponse(url string, token string) (*http.Response, error) {
    req, _ := http.NewRequest(http.MethodGet, url, nil)

    // add authorization header to avoid a 401 error
    req.Header.Add("Authorization", authorizationValue(token))
    // add SSE (server sent event) client header to receive events
    req.Header.Add("Accept", "text/event-stream")

    customClient := http.Client {
        CheckRedirect: func(redirRequest *http.Request, via []*http.Request) error {
            // Forward headers in case there is a redirect 3xx response is received.
            redirRequest.Header = req.Header

            // Go's http.DefaultClient allows 10 redirects before returning an
            // an error. We have mimicked this default behavior.
            if len(via) >= 10 {
                return errors.New("stopped after 10 redirects")
            }
            return nil
        },
    }

    resp, err := customClient.Do(req)
    if err != nil {
        panic(fmt.Sprintf(
            "Error occurred connecting to %s with token %s: %s", 
            url, token, err,
        ))
    }
    if resp.StatusCode != http.StatusOK {
        panic(fmt.Sprintf(
            "Expected a %d status code; got a %d",
            http.StatusOK, resp.StatusCode,
        ))
    }
    return resp, err
}
Node.js
'use strict'

const EventSource = require('eventsource');

const NEST_API_URL = 'https://developer-api.nest.com';

var token = 'YOUR_TOKEN_HERE`; // Update with your token

startStreaming(token);

/**
 * Start REST streaming device events given a Nest token.
 */
function startStreaming(token) {
    var headers = {
        "Authorization": 'Bearer ' + token
    }
    var source = new EventSource(NEST_API_URL, {"headers": headers});

    source.addEventListener('put', function(event) {
        console.log('\n' + event.data); // Nest data in JSON format
    });

    source.addEventListener('open', function(event) {
        console.log('Connection opened!');
    });

    source.addEventListener('auth_revoked', function(event) {
        console.log('Authentication token was revoked.');
        // Re-authenticate your user here.
    });

    source.addEventListener('error', function(event) {
        if (event.readyState == EventSource.CLOSED) {
            console.error('Connection was closed!', event);
        } else {
            console.error('An unknown error occurred: ', event);
        }
    }, false);
}

To use the Node.js code example, install the eventsource module (https://www.npmjs.com/package/eventsource).

npm install eventsource
Python 2
import sseclient # see install information below
import urllib3

NEST_API_URL = 'https://developer-api.nest.com'

token = "YOUR_TOKEN_HERE" # Update with your token

def get_data_stream(token, api_endpoint):
    """ Start REST streaming device events given a Nest token.  """
    headers = {
        'Authorization': "Bearer {0}".format(token),
        'Accept': 'text/event-stream'
    }
    url = api_endpoint
    http = urllib3.PoolManager()
    response = http.request('GET', url, headers=headers, preload_content=False)
    client = sseclient.SSEClient(response)
    for event in client.events(): # returns a generator
        event_type = event.event
        print "event: ", event_type
        if event_type == 'open': # not always received here
            print "The event stream has been opened"
        elif event_type == 'put':
            print "The data has changed (or initial data sent)"
            print "data: ", event.data
        elif event_type == 'keep-alive':
            print "No data updates. Receiving an HTTP header to keep the connection open."
        elif event_type == 'auth_revoked':
            print "The API authorization has been revoked."
            print "revoked token: ", event.data
        elif event_type == 'error':
            print "Error occurred, such as connection closed."
            print "error message: ", event.data
        else:
            print "Unknown event, no handler for it."

get_data_stream(token, NEST_API_URL)

To use the Python 2 code example, install sseclient-py (https://pypi.python.org/pypi/sseclient-py/1.7).

pip install sseclient-py
Python 3
import sseclient # see install information below
import urllib3

NEST_API_URL = 'https://developer-api.nest.com'

token = "YOUR_TOKEN_HERE" # Update with your token

def get_data_stream(token, api_endpoint):
    """ Start REST streaming device events given a Nest token.  """
    headers = {
        'Authorization': "Bearer {0}".format(token),
        'Accept': 'text/event-stream'
    }
    url = api_endpoint
    http = urllib3.PoolManager()
    response = http.request('GET', url, headers=headers, preload_content=False)
    client = sseclient.SSEClient(response)
    for event in client.events(): # returns a generator
        event_type = event.event
        print("event: ", event_type)
        if event_type == 'open': # not always received here 
            print("The event stream has been opened")
        elif event_type == 'put':
            print("The data has changed (or initial data sent")
            print("data: ", event.data)
        elif event_type == 'keep-alive':
            print("No data updates. Receiving an HTTP header to keep the connection open.")
        elif event_type == 'auth_revoked':
            print("The API authorization has been revoked.")
            print("revoked token: ", event.data)
        elif event_type == 'error':
            print("Error occurred, such as connection closed.")
            print("error message: ", event.data)
        else:
            print("Unknown event, no handler for it.")

get_data_stream(token, NEST_API_URL)

To use the Python 3 code example, install sseclient-py (https://pypi.python.org/pypi/sseclient-py/1.7).

python3 -m pip install sseclient-py
Java
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Authenticator;
import okhttp3.Route;
import okio.Buffer;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class RestStreamClient {
    private static final Long DEFAULT_BYTE_COUNT = 2048L;

    String accessToken = "Bearer YOUR_TOKEN_HERE"; // Update with your token
    String nestApiUrl = "https://developer-api.nest.com";
    private OkHttpClient httpClient;
    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    public static void main(String[] args) {
        new RestStreamClient().start();
    }

    public void start() {
        httpClient = new OkHttpClient().newBuilder()
            .authenticator(new Authenticator() {
                @Override public Request authenticate(Route route, Response response) {
                    return response.request().newBuilder()
                            .header("Authorization", accessToken)
                            .build();
                }
            })
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .build();

        executorService.execute(new RestStreamClient.Reader());
    }

    private class Reader implements Runnable {
        @Override
        public void run() {
            Response response = null;
            Request request = new Request.Builder()
                    .url(nestApiUrl)
                    .addHeader("Accept", "text/event-stream")
                    .build();
            try {
                response = httpClient.newCall(request).execute();
                Buffer buffer = new Buffer();
                while (!response.body().source().exhausted()) {
                    long count = response.body().source().read(buffer, DEFAULT_BYTE_COUNT);
                    if (count > 0) {
                        String msg = segment(buffer.readUtf8());
                        parse(msg);
                    }
                }
            } catch(Exception ex) {
                ex.printStackTrace();
            } finally {
                if (response != null) response.body().close();
            }
        }

        private void parse(String msg) throws Exception {
            if (msg == null) return;
            String[] lines = msg.split("\n");
            int i = 0;
            while(i < lines.length) {
                String currentLine = lines[i];
                if (currentLine.startsWith("{\"error\":")) {
                    System.out.println("An error occurred! " +  currentLine);
                } else if (currentLine.startsWith("event:") && lines.length > i + 1) {
                    String nextLine = lines[i + 1];
                    if (currentLine.length() <= 8) {
                        throw new Exception("Unexpected length of event line.");
                    }
                    if (nextLine.length() <= 7) {
                        throw new Exception("Unexpected length of data line.");
                    }
                    String eventType = currentLine.substring(7); //7 = length of("event: ")
                    String json = nextLine.substring(6); //6 = length of("data: ")
                    System.out.println("\nevent: " +  eventType);
                    System.out.println("data: " +  json);
                }
                i++;
            }
        }

        private String segment(String buf) {
            if (buf.endsWith("\n") || buf.endsWith("}")) {
                String msg = accumulator + buf;
                accumulator = "";
                return msg;
            } else accumulator = accumulator + buf;
            return null;
        }

        private String accumulator = "";
    }
}

To use the Java code example:

1. Download the latest okhttp and okio Jar files at http://square.github.io/okhttp/.

2. Construct your code.

3. Compile and run the program with the following class-path settings.

javac -cp .:okhttp-3.9.1.jar:okio-1.13.0.jar RestStreamClient.java
java -cp .:okhttp-3.9.1.jar:okio-1.13.0.jar RestStreamClient

REST streaming and rate limits

To prevent overutilization of the Nest service, we limit the number of connections a product can make in a specific time period. For REST streaming calls, each access token has a limited number of read calls.

For more info, see Data Rate Limits.

REST streaming and redirects

Another consideration of using REST streaming is that your Works with Nest product must also handle 307 redirects.

REST streaming and HTTP client libraries

When you choose an HTTP client library, be sure that it supports streaming.

For instance, the client libraries below use the EventSource interface to receive server-sent events.

Works with Nest connection closed

If a user removes a Works with Nest connection, then your product receives an auth_revoked event and the connection closes.