How to use the API with a backend-only service

Hello, I’m trying to use the SmartThings API to list devices and run commands for them using a Node.js server. There is a lot of old documentation published still, but from my understanding, using a PAT is not advised anymore since they expire in 24hours (unless im wrong, there is a lot of conflicting info on the docs about this). I’m trying to use an auth flow. I made a Smart App to obtain a client id and secret, I registered it using the link sent to my https host, and I’m attempting to get an access token through

const data = qs.stringify({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecret,
    });

    const headers = {
      'Content-Type': 'application/x-www-form-urlencoded',
    };
    const response = await axios.post('https://api.smartthings.com/oauth/token', data, { headers });

But I get an error ERR_BAD_REQUEST, yet it also later in the error says Unauthorized:

Error getting access token: AxiosError: Request failed with status code 401
    at settle (/Users/peter/Desktop/programs/SmartHome/source_code/server/node_modules/axios/dist/node/axios.cjs:1967:12)
    at IncomingMessage.handleStreamEnd (/Users/peter/Desktop/programs/SmartHome/source_code/server/node_modules/axios/dist/node/axios.cjs:3062:11)
    at IncomingMessage.emit (node:events:530:35)
    at endReadableNT (node:internal/streams/readable:1696:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'ERR_BAD_REQUEST',
  config: {
    transitional: {
      silentJSONParsing: true,
      forcedJSONParsing: true,
      clarifyTimeoutError: false
    },
    adapter: [ 'xhr', 'http' ],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    env: { FormData: [Function], Blob: [class Blob] },
    validateStatus: [Function: validateStatus],
    headers: Object [AxiosHeaders] {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'axios/1.6.2',
      'Content-Length': '127',
      'Accept-Encoding': 'gzip, compress, deflate, br'
    },
    method: 'post',
    url: 'https://api.smartthings.com/v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9',
    data: 'grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9'
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      socket: [Function (anonymous)],
      timeout: [Function (anonymous)],
      finish: [Function: requestOnFinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: true,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: '127',
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    _closed: true,
    socket: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      secureConnecting: false,
      _SNICallback: null,
      servername: 'api.smartthings.com',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 9,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'api.smartthings.com',
      _closeAfterHandlingError: false,
      _readableState: [ReadableState],
      _writableState: [WritableState],
      allowHalfOpen: false,
      _maxListeners: undefined,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: [TLSWrap],
      _requestCert: true,
      _rejectUnauthorized: true,
      timeout: 5000,
      parser: null,
      _httpMessage: null,
      autoSelectFamilyAttemptedAddresses: [Array],
      [Symbol(alpncallback)]: null,
      [Symbol(res)]: [TLSWrap],
      [Symbol(verified)]: true,
      [Symbol(pendingSession)]: null,
      [Symbol(async_id_symbol)]: -1,
      [Symbol(kHandle)]: [TLSWrap],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: 5000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 19518,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 94,
        [Symbol(triggerId)]: 92
      },
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: false,
      [Symbol(kSetKeepAlive)]: true,
      [Symbol(kSetKeepAliveInitialDelay)]: 1,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(connect-options)]: [Object]
    },
    _header: 'POST /v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9 HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'Content-Type: application/x-www-form-urlencoded\r\n' +
      'User-Agent: axios/1.6.2\r\n' +
      'Content-Length: 127\r\n' +
      'Accept-Encoding: gzip, compress, deflate, br\r\n' +
      'Host: api.smartthings.com\r\n' +
      'Connection: keep-alive\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 443,
      protocol: 'https:',
      options: [Object: null prototype],
      requests: [Object: null prototype] {},
      sockets: [Object: null prototype] {},
      freeSockets: [Object: null prototype],
      keepAliveMsecs: 1000,
      keepAlive: true,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      maxCachedSessions: 100,
      _sessionCache: [Object],
      [Symbol(shapeMode)]: false,
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'POST',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    joinDuplicateHeaders: undefined,
    path: '/v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9',
    _ended: true,
    res: IncomingMessage {
      _events: [Object],
      _readableState: [ReadableState],
      _maxListeners: undefined,
      socket: null,
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      rawHeaders: [Array],
      rawTrailers: [],
      joinDuplicateHeaders: undefined,
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 401,
      statusMessage: 'Unauthorized',
      client: [TLSSocket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      _eventsCount: 4,
      responseUrl: 'https://api.smartthings.com/v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9',
      redirects: [],
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false,
      [Symbol(kHeaders)]: [Object],
      [Symbol(kHeadersCount)]: 38,
      [Symbol(kTrailers)]: null,
      [Symbol(kTrailersCount)]: 0
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: 'api.smartthings.com',
    protocol: 'https:',
    _redirectable: Writable {
      _events: [Object],
      _writableState: [WritableState],
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 127,
      _requestBodyBuffers: [],
      _eventsCount: 3,
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'https://api.smartthings.com/v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9',
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false
    },
    [Symbol(shapeMode)]: false,
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'content-type': [Array],
      'user-agent': [Array],
      'content-length': [Array],
      'accept-encoding': [Array],
      host: [Array]
    },
    [Symbol(errored)]: null,
    [Symbol(kHighWaterMark)]: 16384,
    [Symbol(kRejectNonStandardBodyWrites)]: false,
    [Symbol(kUniqueHeaders)]: null
  },
  response: {
    status: 401,
    statusText: 'Unauthorized',
    headers: Object [AxiosHeaders] {
      date: 'Wed, 05 Feb 2025 02:54:06 GMT',
      'content-type': 'application/json',
      'content-length': '0',
      connection: 'keep-alive',
      server: 'openresty',
      'trace-id': '51c2b69e7d2a85b4',
      vary: 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers',
      'www-authenticate': 'Basic realm="oauth2/client"',
      'x-content-type-options': 'nosniff',
      'x-xss-protection': '1; mode=block',
      'x-frame-options': 'DENY',
      'x-token-type': 'ST',
      'x-varnish': '332724703',
      age: '0',
      via: '1.1 varnish (Varnish/6.2)',
      'cache-control': 'no-cache, no-store, max-age=0, must-revalidate',
      pragma: 'no-cache',
      expires: 'Wed, 01 Jan 1800 00:00:00 GMT',
      'access-control-allow-origin': '*'
    },
    config: {
      transitional: [Object],
      adapter: [Array],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      env: [Object],
      validateStatus: [Function: validateStatus],
      headers: [Object [AxiosHeaders]],
      method: 'post',
      url: 'https://api.smartthings.com/v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9',
      data: 'grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9'
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: true,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: '127',
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      _closed: true,
      socket: [TLSSocket],
      _header: 'POST /v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9 HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'Content-Type: application/x-www-form-urlencoded\r\n' +
        'User-Agent: axios/1.6.2\r\n' +
        'Content-Length: 127\r\n' +
        'Accept-Encoding: gzip, compress, deflate, br\r\n' +
        'Host: api.smartthings.com\r\n' +
        'Connection: keep-alive\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: nop],
      agent: [Agent],
      socketPath: undefined,
      method: 'POST',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      joinDuplicateHeaders: undefined,
      path: '/v1/oauth/token?grant_type=client_credentials&client_id=b16b7dbe-ea06-41a2-9c0c-d3ec1dd477eb&client_secret=a309c391-490e-4dbc-833a-e16599c49fe9',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: 'api.smartthings.com',
      protocol: 'https:',
      _redirectable: [Writable],
      [Symbol(shapeMode)]: false,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(errored)]: null,
      [Symbol(kHighWaterMark)]: 16384,
      [Symbol(kRejectNonStandardBodyWrites)]: false,
      [Symbol(kUniqueHeaders)]: null
    },
    data: ''
  }
}

I’m of course positive the client id and secret is correct. I feel like it shouldn’t be this difficult. Thank you so much!

I’ve not come across the client credentials flow being used with SmartThings before. That’s not to say it might not be used internally but it is usually the authorization code flow that is seen.

SmartThings have SDKs to support using Node.js SmartApps, as well as example OAuth-In apps, which have also been known as API Access apps (example).

I posted a comment in another thread showing the flow outside the context of actual apps.

The deal with PATs is that ST have become concerned by potentially extremely powerful tokens with fifty year lifespans being used in production contexts that they weren’t ever intended for. Freshly created PATs now have a 24 hour lifespan and in use the API is rate limited in certain contexts. Existing PATs created before the end of last year remain as they were.

Unfortunately PATs can’t be effectively replaced by SmartApp tokens in all the cases they have found themselves in. SmartApp tokens work within single Location contexts which is fine for day to day operation. PATs also allow you to do administrative tasks at the user account level. Setup of apps is an example.

Thank you so much for the response! From my understanding, the authorization code flow would require me to have a frontend - a user would have to authenticate to manage tokens… my issue is its a backend service for home automation that i prefer to just run in the background with no client interaction needed.

Following your other (awesome) comment on the other thread, I believe I can achieve what I want by manually aquiring the first access and refresh tokens, and then have the server refresh the token periodically to achieve the same effect. If I’m wrong about this, I would appreciate a heads up before I drive myself into a rabbit hole, but I feel like I can do the same lifetime access token idea with this route.

Thanks so much

Yes, that is my understanding too. I literally only read about the client credentials flow after seeing your post and my understanding with that is it is the app itself that is being identified and if you want to identify individual users you have to sort that out separately. I could imagine that being something that SmartThings could exploit for internally created tools, making use of the Samsung login details, but externally everything seems to require a user authorization.

I’d literally created my first OAuth-In app (or rather the shell of one) in the days before I wrote that post and I’d done it for real with my own app on a server. It was only while writing that post and doing the whole “and you don’t even need to write an app” thing that I realised the same thing you have. Once you have those first access and refresh tokens it just takes a single POST to refresh them so indeed you have pretty much the same lifetime access token. I can’t see why it wouldn’t work.

1 Like

Hi, @Peter_Buonaiuto
Indeed you need the user to authorize your app at least once so you can get an access token for their account and then you can continue refreshing it to avoid losing access.
The user will see your app in their section of “Linked Services” in the app. This helps them know which apps can access their devices and they can remove the service from there which would make the token invalid.
Just a heads up, you cannot get personal information from the user like their email with that token but you can differentiate them through the installedAppId.