NAV Navbar
Logo
python

Introduction

Welcome to the Sonlet API! You can use our API to access Sonlet API endpoints, allowing you to build on top of our massive Direct Sales ecosystem.

We have example projects set up for a few popular platforms that you can use for reference:

Platform Code
Server-side web app GitHub
Android GitHub
iOS TODO

We’re working on creating proper client libraries for popular languages, but in the meantime you can use any off-the-shelf HTTP request library to get everything done that you need. In this guide we’re using python and requests.

Important URLs

The API and OAUTH base urls for the staging and production sites are:

Staging (https://beta.sonlet.com)

Production (https://sonlet.com)

Authentication

Get an Access Token

Sonlet uses API keys and oauth2 to grant access to the API. You can register a new Sonlet API by sending an email to [email protected]. You might also want to hop on our Slack team to facilitate collaboration.

Once you have credentials for your app, you can use our oauth2 endpoints to get a personal access token for the user. The way you do that depends on what kind of app you’re developing.

Application type Grant type
Mobile app (iOS, Android, etc.) Implicit
Web app (server-side) Authorization code

Implicit Grant (Mobile apps (iOS, Android, etc))

Since the code in a mobile app is distributed to the user, you can’t include your secret key in your code, since that would compromise your secret key and could result in abuse and blacklisting. Instead, you rely on the resource owner (the user) to grant access. The oauth2 specification refers to this as an “implicit” grant type, which you can read more about here.

In order to request a personal access token for a user, just send them to the following URL:

https://sonlet.com/o/authorize/?response_type=token&client_id=$CLIENT_ID&state=random_state_string&redirect_uri=$REDIRECT_URI

Make sure to replace $CLIENT_ID and $REDIRECT_URI with your client ID and desired redirect URI, respectively.

Once the user authenticates and grants access to your application they’ll be redirected to $REDIRECT_URI with an access_token parameter in the query fragment. $REDIRECT_URI should actually be a custom schema (e.g. my-cool-app://str-cb) that you’ve registered with your system. See the Android STR API demo for an example of how to register a custom schema handler and how to parse the access_token out of the URL fragment.

Authorization Code Grant (Server-side Web Apps)

This is the redirect URI handler. It will extract the code, exchange it for a set of tokens, save the tokens, and redirect the user to your app where the access token can be used to make authorized requests to the STR API.

OAUTH_BASE = 'https://sonlet.com/o/'
TOKEN_URL = OAUTH_BASE + 'token/'

# this is the redirect URI callback handler
@route('/cb')
def cb():
    if request.query.get('error'):
        # ...more error logging/handling...
        redirect('/')
        return

    # exchange the auth code for an access token
    data = {
        "grant_type": "authorization_code",
        "code": request.query['code'],
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    res = requests.post(TOKEN_URL, data=data)
    if res.status_code != 200:
        raise AuthenticationFailed(res)
    resp = json.loads(res.content)

    user = get_current_user()
    user.save_tokens(resp['access_token'], resp['refresh_token'])
    redirect('/app')

For server-side apps, you’ll use the secret access key to get a personal access token for the user. The oauth2 spec refers to this as an “Authorization code” grant type, which you can read more about here. This method offers a bit more security and features (like refresh tokens).

As with all secret keys, you should never check your key in to your source code repo. Instead, you can deploy it with your app using environment variables or similar.

The flow for the authorization grant type is a two-step process:

  1. Send an authorization request (server responds with a code)
  2. Send a token request using the code from step 1 (server responds with an access_token)

To get the process started, just send the user to the following URL:

https://sonlet.com/o/authorize/?response_type=code&client_id=$CLIENT_ID&state=random_state_string&redirect_uri=$REDIRECT_URI

Make sure to replace $CLIENT_ID and $REDIRECT_URI with your client ID and desired redirect URI, respectively. The REDIRECT_URI will most likely be a custom endpoint in your app to handle oauth callbacks, for example: https://my-cool-app.example.com/str-cb.

Once the user authenticates and grants access to your application they’ll be redirected to $REDIRECT_URI with a code parameter in the querystring. You can then use the returned code to get access/refresh tokens by posting to $OAUTH_BASE/token/ (see code example at the right).

Refresh Tokens

The access_token can be used right away to make authorized API requests. The refresh_token (oauth2 spec) can be used to get a new access token in the future:

# Swapping out a refresh token for a new access token
data = {
    'client_id': CLIENT_ID,
    'client_secret': CLIENT_SECRET,
    'grant_type': 'refresh_token',
    'refresh_token': REFRESH_TOKEN
}
res = requests.post(oauth_url, data=data)
if res.status_code != 200:
    raise AuthenticationError(res)
resp = json.loads(res.content)
user = get_current_user()
# save the new access/refresh tokens
user.save_tokens(resp['access_token'], resp[refresh_token'])

Generally you’ll only do the refresh if a regular request returns an Unauthorized error (status code 401). So you might use a wrapper like this:

def make_api_request(user, endpoint):
    """Simple wrapper that does a refresh if needed"""
    resp = _make_api_request(user.access_token, endpoint)
    if resp.status_code == 401:
        user.refresh_tokens()
        resp = _make_api_request(user.access_token, endpoint)
    return resp

All personal access tokens have a 14-day expiration on them. In order to avoid making the user re-authorize your app by clicking through the oauth permissions dialog again, you can use the refresh_token that gets returned alongside your access_token to get a new personal access token.

Most of the time you’ll wait to refresh a token until you get an “invalid token error” with the current access token. See this section of the oauth2 spec for an example sequence diagram. There’s also an example on the right showing how you might do it.

Use the Access Token

To make an authorized request, just add an “Authorization” header with a personal access token:

headers = {"Authorization": "Bearer " + access_token}
requests.get(endpoint_url, headers=headers)

Once you have personal access token for a user, you can make API requests on their behalf. Sonlet expects the API key to be included in all API requests to the server in an HTTP header that looks like the following:

Authorization: Bearer <token>

Idempotency

# Duplicate request WITHOUT idempotency protection:
$ curl $ENDPOINT -d $DATA \
    -H 'Authorization: Bearer ...' \
    -H "Content-Type: application/json"
{"pk":15308091, ...}

$ curl $ENDPOINT -d $DATA \
    -H 'Authorization: Bearer ...' \
    -H "Content-Type: application/json"
{"pk":15308092, ...}

# Duplicate request WITH idempotency protection:
$ curl $ENDPOINT -d $DATA \
    -H 'Authorization: Bearer ...' \
    -H "Content-Type: application/json" \
    -H 'X-Idempotency-Key: pizza123'
{"pk":15308093, ...}

$ curl $ENDPOINT -d $DATA \
    -H 'Authorization: Bearer ...' \
    -H "Content-Type: application/json" \
    -H 'X-Idempotency-Key: pizza123'
{"detail":"Duplicate request detected."}

The STR API can optionally enforce idempotency on HTTP POST requests. This is to avoid the following scenario:

  1. Client sends HTTP POST to create invitem.
  2. STR backend creates the invitem and commits it to the database.
  3. Client loses network connectivity.
  4. STR backend tries to send back a success response, but is unable to do so since the client lost network.
  5. The client never gets the “success” response, so assumes that the request fails. Client retries the request, creating a duplicate invitem.

A client can activate idempotency enforcement via the X-Idempotency-Key HTTP header. The client can pass any arbitrary string, up to 128 characters. If another request comes in from the same user within 24 hours using the same value for the X-Idempotency-Key header the request will return an HTTP 403 with the error message: "Duplicate request detected.". See the shell session on the right for an example.

API Schema

The remainder of this document will provide details on a few key endpoints and their usage.

Items

Get All Items

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'items/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['results'][0]['image']['image_thumbnail']

This endpoint retrieves all items. You can see what the response looks like in the API browser. The response is paged.

Get a Specific Item

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'items/<ITEM_ID>/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['image']['image_thumbnail']

This endpoint retrieves a specific item.

Create an Item

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
data = {
    'itemchoice_id': itemchoice_id,
    'size_id': size_id,
    'image_id': image_id,
    'quantity': 4,
    'data': json.dumps({  # optional!
        'attributes': {
            'Color': 'Red',
            'Collection': 'Elegant',
        }
    }),
}
res = requests.post(api_url + 'items/', headers=headers, data=data)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['image_thumbnail'], data['pk']

Create an item object by POSTing to $API_BASE/items/.

Required:

Optional:

The full list of fields that can be set is in the API schema.

Images

Get All Images

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'images/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['results'][0]['image_thumbnail']

This endpoint retrieves all images. You can see what the response looks like in the API browser. The response is paged.

Get a Specific Image

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'images/<IMAGE_ID>/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['image_thumbnail']

This endpoint retrieves a specific image.

Create an Image

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
with open('/tmp/my_image.png') as imgfile:
    files = {'image_full': imgfile}
    res = requests.post(api_url + 'images/', headers=headers, files=files)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['image_thumbnail'], data['pk']

You can create an image object by POSTing to $API_BASE/images/, providing a file object in the image_full parameter. Images aren’t terribly useful on their own. Generally you should also create an Item to go along with the Image.

Item Choices (a.k.a. “styles”)

Get All Item Choices

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'itemchoices/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['results'][0]

This endpoint retrieves all item choices. You can see what the response looks like in the API browser. The response is paged.

Get a Specific Item Choice

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'itemchoices/<ITEMCHOICE_ID>/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data

This endpoint retrieves a specific item choice.

Sizes

Get All Sizes

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'sizes/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data['results'][0]

This endpoint retrieves all sizes. You can see what the response looks like in the API browser. The response is paged.

Get a Specific Size

import requests

api_url = 'https://sonlet.com/api/v2/'
headers = {"Authorization": "Bearer " + access_token}
res = requests.get(api_url + 'sizes/<SIZE_ID>/', headers)
if res.status_code != 200:
    raise AuthenticationError(res)
data = json.loads(res.content)
print data

This endpoint retrieves a specific size.