SnapTrade

Getting Started

Welcome to the SnapTrade developer hub. You'll find comprehensive documentation for all our endpoints, as well as detailed guides that will be added shortly.

If you need help or have any questions, send us an email at [email protected].


API Status

Call the API Status endpoint to make sure the API is live and that you can make the most basic request. You should receive a response indicating the API status, current server timestamp, and internal API minor version number.

Example:

{"version":151,"timestamp":"2022-01-24T19:02:46.440074Z","online":true}

Generating Request Signatures

In order to make an authenticated request to the SnapTrade API, you will need to generate a signature for each request that proves you hold the consumer key that was created for you.

Steps to generate a signature

In order to create the signature, you will need to start by constructing the signature content, which is a string that contains all the relevant request information that needs to be signed. The signature content is a serialized JSON object that is rendered in a specific, non-ambiguous way.

Once you have defined the signature content, you then use your consumer key to produce a cryptographic digest of the signature content.

Finally, you convert the digest into its Base64 representation and attach it to the request in the Signature header. This signature is sufficient for SnapTrade to be able to verify that the entire request was generated by you and not modified in transit.

At a high level, here are the steps

  1. Prepare the signature content, which will be signed in the next step.
  2. Sign the signature content (encoded as UTF-8) with HMAC-SHA256 using your consumer key.
  3. Include the Base64 encoding of the signed content as the Signature header.

📘

Note

Use the consumer key provided by SnapTrade to sign the request (not the example included here).

How to prepare the signature content

The signature content is a JSON object with 3 required key-value pairs:

  • content: Content which is passed along as the request body. Set this value to null for empty request bodies (for example, GET requests or POST requests with no body).
  • path: The URL path for the request.
  • query: The exact query param string that is included in the request. (ex: "clientId=PASSIVTEST&timestamp=1635790389"

After creating the JSON object with all the required information, it must be rendered as a flat string so that it can be signed. The JSON spec has some flexibility on whitespace, but cryptographic digests do not. For example, while the following two JSON strings would be parsed to result in the same JSON object, they would NOT produce the same cryptographic digest:

  • {"hello":"world","blue":"moon"} <-- this is the correct format to use for SnapTrade
  • {"hello": "world", "blue": "moon"}

As a result, we need to be very specific about how to render the signature content JSON object as a flat string. The requirements are as follows:

  1. Sort keys alphabetically.

  2. Remove any extra whitespace characters from the signed content.

  3. Render as a UTF-8 encoded string.

Sample signature code

Here are a few examples of how to produce the signature in several languages. If your language isn't included here, contact SnapTrade support and we will help you figure it out!

import hmac
import json
from base64 import b64encode
from hashlib import sha256
from urllib.parse import urlencode

consumer_key = "YOUR_CONSUMER_KEY".encode()

request_data = {'userId': 'new_user_123'}
request_path = "/api/v1/snapTrade/registerUser"
request_query = "clientId=PASSIVTEST&timestamp=1635790389"

sig_object = {"content": request_data, "path": request_path, "query": request_query}

sig_content = json.dumps(sig_object, separators=(",", ":"), sort_keys=True)
sig_digest = hmac.new(consumer_key, sig_content.encode(), sha256).digest()

signature = b64encode(sig_digest).decode()
const crypto = require("crypto");

const JSONstringifyOrder = (obj) => {
  var allKeys = [];
  var seen = {};
  JSON.stringify(obj, function (key, value) {
    if (!(key in seen)) {
      allKeys.push(key);
      seen[key] = null;
    }
    return value;
  });
  allKeys.sort();
  return JSON.stringify(obj, allKeys);
};

const consumerKey = encodeURI('YOUR_CONSUMER_KEY');

const requestData = {'userId': 'new_user_123'}
const requestPath = "/api/v1/snapTrade/registerUser"
const requestQuery = "clientId=PASSIVTEST&timestamp=1635790389"

const sigObject = {"content": requestData, "path": requestPath, "query": requestQuery}

const sigContent = JSONstringifyOrder(sigObject)

const hmac = crypto.createHmac("sha256", consumerKey);
const signature = hmac.update(sigContent).digest('base64');
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class main {
    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
        System.out.println("Hello, World!");
        final String data = "{\"content\":{\"userId\":\"new_user_123\"},\"path\":\"/api/v1/snapTrade/registerUser\",\"query\":\"clientId=PASSIVTEST&timestamp=1635790389\"}";

        final String key = "YOUR_CONSUMER_KEY";
        final String algorithm = "HmacSHA256";
        final SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm);
        final Mac mac = Mac.getInstance(algorithm);
        mac.init(secretKeySpec);
        System.out.println(data);
        final String base64 = new String(Base64.encodeBase64(mac.doFinal(data.getBytes())));
        System.out.println(base64);
    }
}
package main

import (
    "io/ioutil"
    "net/http"
    "bytes"
    "fmt"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/json"
    "encoding/base64"
    "time"
    "strconv"
 )
 type QueryParams struct {
    ClientId string     `json:"clientId"`
    Timestamp int64     `json:"timestamp"`
    UserId string       `json:"userId"`
    UserSecret string   `json:"userSecret"`
}
 type Signature struct {
    Content map[string]interface{}  `json:"content"`
    Path string                     `json:"path"`
    Query string                    `json:"query"`
}

func main() {
    var sig_content = new(Signature)
    var timestamp = time.Now().Unix()

    var client_id = "YOUR_CLIENT_ID"
    var user_id = "USER_ID"
    //var user_secret = "USER_SECRET"   Needed for user based requests but not registerUser
    var consumer_key = "YOUR_CONSUMER_KEY"
    var url = "https://api.snaptrade.com/api/v1/snapTrade/registerUser"

    sig_content.Content = make(map[string]interface{})
    sig_content.Content["userId"] = user_id
    sig_content.Path = "/api/v1/snapTrade/registerUser"
    sig_content.Query = "clientId=" + client_id + "&timestamp="+ strconv.FormatInt(timestamp,10)
    
    sig_json, _ := JSONMarshal(sig_content)

    var signature = ComputeHmac256(string(sig_json), consumer_key)
    
    r, _ := json.Marshal(sig_content.Content)

    reqBody := bytes.NewBuffer(r)

    req, _ := http.NewRequest("POST", url, reqBody)
    req.URL.RawQuery = sig_content.Query

    req.Header.Add("accept", "application/json")
    req.Header.Add("content-type", "application/json")
    req.Header.Add("Signature", signature)

    res, _ := http.DefaultClient.Do(req)

    defer res.Body.Close()
    body, _ := ioutil.ReadAll(res.Body)

    fmt.Println(res)
    fmt.Println(string(body))
}

func ComputeHmac256(message string, secret string) string {
    key := []byte(secret)
    h := hmac.New(sha256.New, key)
    h.Write([]byte(message))
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func JSONMarshal(t interface{}) ([]byte, error) {
    buffer := &bytes.Buffer{}
    encoder := json.NewEncoder(buffer)
    encoder.SetEscapeHTML(false)
    err := encoder.Encode(t)
    json := buffer.Bytes()
    json = bytes.TrimRight(json, "\n")
    return json, err
}

Register a test user

To create a secure brokerage authorization, we first need to register a test user.

Call the Register user endpoint with a userId which can be any string as long as it's unique to a user. Upon receiving a 200 response, the user is registered successfully and you should receive a response with a user id and a randomly generated user secret. You can think of the user secret like a per-user API key that provides an additional layer of security for protecting user data. The user id and user secret need to be passed along to all SnapTrade API endpoints that involve access to user data.

Example:

{"userId":"USERID123","userSecret": "AYSEY8726837292873"}

Generate a login link

SnapTrade partners need to generate a redirect URI for a user so they can securely log in to the SnapTrade Connection portal and connect their accounts.

The redirect URI can be generated by sending a POST request to the Login endpoint. userId and userSecret (previously generated through calling the registerUser endpoint) have to be in the query parameters of the POST request.

The response would indicate a redirectURI to be used for login a user to the SnapTrade portal.

Example:

{"redirectURI": "https://app.snaptrade.com/snapTrade/redeemToken?token=TOKEN&clientId=PASSIVTEST"}

📘

Note

If trading is enabled for a partner, users also need to provide a PIN (or create one if a first-time user)
before logging into the SnapTrade portal.


Connect an account through the Connection Portal

Once a user successfully logs into the SnapTrade portal, they can select their brokerage and go through the OAuth flow to connect their brokerage accounts.

📘

Note

For the initial connection, partners can pass in a broker name in the URI so users can go directly to the selected brokerage and make a connection.

Example: https://app.snaptrade.com/snapTrade/redeemToken?token=TOKEN&clientId=PASSIVTEST&broker=Questrade


Pull user holdings

In order to retrieve user holdings for a specific account, you can call the Holdings endpoint by passing the clientId, timestamp, userId and list of account numbers (accounts) to filter the holdings.

In the response, you should get an array of objects containing each account holdings data.

Example:

[
{
  "account": {
    "id": "908192",
    "brokerage": "X",
    "number": "123456",
    "name": "Y"
  },
  "balances": [
    {
      "currency": {
        "id": "123",
        "code": "CA",
        "name": "Canadian dollar"
      },
      "cash": 1200
    }
  ],
  "positions": [
    {
      "symbol": {
        "symbol": "SYMBL",
        "name": "Symbol name",
        "currency": {
          "id": "123",
          "code": "CA",
          "name": "Canadian dollar"
        },
        "exchange": {
          "code": "123123",
          "name": "X"
        }
      },
      "units": "1",
      "price": 12
    }
  ]
}
]

Place a non-marketable order

To place an order through SnapTrade API, you need to go through the following two steps:

1- To receive information on how a specific order will impact an account, send a POST request to Trade Impact endpoint. The following example is how the body of the request should look like:

{
   "account_id":"123456",
   "universal_symbol_id":"78910",
   "order_type":"Limit",
   "time_in_force":"Day",
   "action":"BUY",
   "units":1,
   "price":12.90
}

Example of the response:

{
   "trade":{
      "id":"0987654321",
      "account":"ACCOUNTID",
      "order_type":"Limit",
      "time_in_force":"Day",
      "symbol":{
         "brokerage_symbol_id":"1234567",
         "universal_symbol_id":"8912345",
         "currency":{
            "id":"012345",
            "code":"USD"
         },
         "local_id":"121314",
         "description":"SYMBOL",
         "symbol":"SYMBL"
      },
      "action":"BUY",
      "units":1,
      "price":12.90
   },
   "trade_impacts":[
      {
         "account":"778899",
         "currency":"023901",
         "remaining_cash":-466.0569,
         "estimated_commissions":0.0,
         "forex_fees":0.0
      },
      {
         "account":"12132214",
         "currency":"f123123",
         "remaining_cash":667.8219,
         "estimated_commissions":0.0,
         "forex_fees":0.0
      }
   ],
   "combined_remaining_balance":{
      "account":"99881882",
      "currency":"57fs1223",
      "cash":64.5564
   }
}

🚧

Note

An order impact request can fail with a 400 response code due to reasons such as Markets are not open, Not enough cash to place trades, Exchange does not support market orders, etc. The reason for a failed request would be in the body of the response.

Example of the response for a failed request:

{"detail":"Not enough cash to place trades","status_code":400,"code":"1068"}

2- To place the order you need to send in the trade id (received through calling the Trade Impact endpoint) as a query parameter to the Execute Trade endpoint. The successful request would indicate the status of the order (EXECUTED, ACCEPTED, FAILED, REJECTED, PENDING, etc.) along with other information related to the order placed.

Example of the response:

{
   "brokerage_order_id":"123456",
   "status":"ACCEPTED",
   "symbol":"7891011",
   "universal_symbol":{
      "id":"123456",
      "symbol":"SYMBL",
      "description":"Symbol",
      "currency":{
         "id":"1021993",
         "code":"USD",
         "name":"US Dollar"
      },
      "currencies":[
      ],
      "type":{
         "id":"ceb92399",
         "code":"code",
         "is_supported":true
      }
   },
   "action":"BUY",
   "total_quantity":"1.00000000",
   "open_quantity":"1.00000000",
   "canceled_quantity":"0.00000000",
   "filled_quantity":"0.00000000",
   "execution_price":"0.0000",
   "limit_price":"12.9000",
   "stop_price":null,
   "order_type":"Limit",
   "time_in_force":"Day",
   "time_placed":"2022-01-01T10:08:09.044000-05:00",
   "time_updated":"2022-01-01T10:08:09.113000-05:00",
   "expiry_date":null
}

Delete a non-marketable order

To cancel an open order, you need to POST to the Cancel Order endpoint with the account id in the query params and the brokerage order id in the body of the request.