Authentication

Service Clients

OAuth2 client credentials flow for server-to-server authentication

Service clients use the OAuth2 client credentials flow for machine-to-machine authentication. This is the standard authentication method for API keys, the WordPress plugin, and custom integrations.

Overview

The client credentials flow exchanges your client_id and client_secret for a JWT access token:

┌─────────────┐                              ┌─────────────┐
│   Client    │                              │  Synoveo    │
│ (WordPress) │                              │    API      │
└──────┬──────┘                              └──────┬──────┘
       │                                            │
       │  POST /auth/token                          │
       │  { client_id, client_secret }              │
       │ ──────────────────────────────────────────>│
       │                                            │
       │  { access_token, refresh_token }           │
       │ <──────────────────────────────────────────│
       │                                            │
       │  GET /api/v1/locations                     │
       │  Authorization: Bearer {access_token}      │
       │ ──────────────────────────────────────────>│
       │                                            │
       │  { locations: [...] }                      │
       │ <──────────────────────────────────────────│
       │                                            │

Token Endpoint

Endpoint: POST /api/v1/auth/token

Client Credentials Grant

Exchange API credentials for tokens:

POST /api/v1/auth/token HTTP/1.1
Host: api.synoveo.com
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "syncid_570_1703030400000_my_app",
  "client_secret": "your_base64_encoded_secret"
}

Request Parameters:

ParameterTypeRequiredDescription
grant_typestringYesMust be "client_credentials"
client_idstringYesYour API key client ID
client_secretstringYesYour API key secret (base64)
permissionsarrayNoRequested permissions (subset of allowed)

Response:

{
  "status": "ok",
  "data": {
    "token_type": "Bearer",
    "scope": "service",
    "plan": "pro",
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 7776000,
    "permissions": ["business.read", "business.write"],
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}

Response Fields:

FieldTypeDescription
token_typestringAlways "Bearer"
scopestringAlways "service" for API keys
planstringUser's subscription plan
access_tokenstringJWT token for API requests
expires_innumberToken TTL in seconds (default: 7,776,000 = 90 days)
permissionsarrayGranted permissions
refresh_tokenstringToken for obtaining new access tokens

Refresh Token Grant

When access tokens expire, use the refresh token:

POST /api/v1/auth/token HTTP/1.1
Host: api.synoveo.com
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Request Parameters:

ParameterTypeRequiredDescription
grant_typestringYesMust be "refresh_token"
refresh_tokenstringYesValid refresh token

Response: Same format as client credentials grant.


Permission Scoping

Request specific permissions to limit token access:

POST /api/v1/auth/token HTTP/1.1
Host: api.synoveo.com
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "syncid_570_...",
  "client_secret": "...",
  "permissions": ["business.read"]
}

Permission Resolution

The granted permissions are the intersection of:

  1. Permissions allowed by the API key
  2. Permissions requested in the token request
API Key AllowsRequestedGranted
["business.read", "business.write"]["business.read"]["business.read"]
["business.read", "business.write"](none)["business.read", "business.write"]
["business.read"]["business.write"][] (error)

Available Permissions

PermissionDescription
business.readRead locations, profiles, reviews, posts
business.writeUpdate profiles, create posts, sync data

Token Characteristics

Service Tokens vs User Tokens

AspectService TokenUser Token
Scope"service""user"
Issued viaClient credentialsGoogle OAuth
AccessSingle assigned locationAll user locations
PermissionsExplicit in tokenFull (implicit)
Default TTL90 days7 days

Token Validation

Service tokens are validated by:

  1. RS256 Signature - Verified with API's public key
  2. Issuer - Must be https://api.synoveo.com
  3. Audience - Must be https://api.synoveo.com
  4. Expiration - exp claim must be in future
  5. API Key Status - Underlying key must be active

Error Handling

Invalid Credentials

{
  "status": "error",
  "error": {
    "code": "AUTH_INVALID_TOKEN",
    "message": "Invalid client credentials"
  }
}

Deactivated API Key

{
  "status": "error",
  "error": {
    "code": "AUTH_INSUFFICIENT_PERMISSIONS",
    "message": "API key has been deactivated",
    "details": {
      "deactivation_reason": "billing_issue",
      "deactivated_at": "2025-01-15T10:00:00Z",
      "upgrade_url": "https://app.synoveo.com/dashboard/billing"
    }
  }
}

Invalid Refresh Token

{
  "status": "error",
  "error": {
    "code": "AUTH_INVALID_TOKEN",
    "message": "Invalid or expired refresh token"
  }
}

Implementation Examples

Node.js

class SynoveoAuth {
  constructor(clientId, clientSecret) {
    this.clientId = clientId
    this.clientSecret = clientSecret
    this.token = null
    this.refreshToken = null
    this.expiresAt = null
  }

  async getToken() {
    // Return cached token if valid (5 min buffer)
    if (this.token && this.expiresAt > Date.now() + 300000) {
      return this.token
    }

    // Try refresh if we have a refresh token
    if (this.refreshToken) {
      try {
        return await this.refresh()
      } catch (e) {
        // Fall through to new token
      }
    }

    return await this.authenticate()
  }

  async authenticate() {
    const response = await fetch('https://api.synoveo.com/api/v1/auth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret
      })
    })

    if (!response.ok) {
      throw new Error('Authentication failed')
    }

    const { data } = await response.json()
    this.token = data.access_token
    this.refreshToken = data.refresh_token
    this.expiresAt = Date.now() + (data.expires_in * 1000)

    return this.token
  }

  async refresh() {
    const response = await fetch('https://api.synoveo.com/api/v1/auth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        refresh_token: this.refreshToken
      })
    })

    if (!response.ok) {
      this.refreshToken = null
      throw new Error('Refresh failed')
    }

    const { data } = await response.json()
    this.token = data.access_token
    this.refreshToken = data.refresh_token
    this.expiresAt = Date.now() + (data.expires_in * 1000)

    return this.token
  }
}

// Usage
const auth = new SynoveoAuth(
  process.env.SYNOVEO_CLIENT_ID,
  process.env.SYNOVEO_CLIENT_SECRET
)

const token = await auth.getToken()
const response = await fetch('https://api.synoveo.com/api/v1/google-business/locations', {
  headers: { 'Authorization': `Bearer ${token}` }
})

PHP (WordPress)

class SynoveoAuth {
    private $clientId;
    private $clientSecret;
    private $tokenOptionKey = 'synoveo_access_token';

    public function __construct($clientId, $clientSecret) {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
    }

    public function getToken() {
        $cached = get_option($this->tokenOptionKey);

        if ($cached && $cached['expires_at'] > time() + 300) {
            return $cached['token'];
        }

        return $this->authenticate();
    }

    private function authenticate() {
        $response = wp_remote_post('https://api.synoveo.com/api/v1/auth/token', [
            'headers' => ['Content-Type' => 'application/json'],
            'body' => json_encode([
                'grant_type' => 'client_credentials',
                'client_id' => $this->clientId,
                'client_secret' => $this->clientSecret
            ])
        ]);

        if (is_wp_error($response)) {
            throw new Exception('Authentication failed');
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);

        update_option($this->tokenOptionKey, [
            'token' => $body['data']['access_token'],
            'refresh_token' => $body['data']['refresh_token'],
            'expires_at' => time() + $body['data']['expires_in']
        ]);

        return $body['data']['access_token'];
    }
}

Python

import requests
import time

class SynoveoAuth:
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self.token = None
        self.refresh_token = None
        self.expires_at = 0

    def get_token(self):
        if self.token and self.expires_at > time.time() + 300:
            return self.token

        if self.refresh_token:
            try:
                return self._refresh()
            except:
                pass

        return self._authenticate()

    def _authenticate(self):
        response = requests.post(
            'https://api.synoveo.com/api/v1/auth/token',
            json={
                'grant_type': 'client_credentials',
                'client_id': self.client_id,
                'client_secret': self.client_secret
            }
        )
        response.raise_for_status()

        data = response.json()['data']
        self.token = data['access_token']
        self.refresh_token = data['refresh_token']
        self.expires_at = time.time() + data['expires_in']

        return self.token

    def _refresh(self):
        response = requests.post(
            'https://api.synoveo.com/api/v1/auth/token',
            json={
                'grant_type': 'refresh_token',
                'refresh_token': self.refresh_token
            }
        )
        response.raise_for_status()

        data = response.json()['data']
        self.token = data['access_token']
        self.refresh_token = data['refresh_token']
        self.expires_at = time.time() + data['expires_in']

        return self.token

Best Practices

  1. Cache Tokens - Reuse tokens until expiration
  2. Proactive Refresh - Refresh 5 minutes before expiration
  3. Handle Errors - Implement retry with exponential backoff
  4. Minimum Permissions - Request only needed permissions
  5. Secure Storage - Never log or expose tokens

SDK Usage

The @synoveo/sdk handles the client credentials flow automatically:

import { SynoveoClient } from '@synoveo/sdk'

const client = new SynoveoClient({
  clientId: process.env.SYNOVEO_CLIENT_ID,
  clientSecret: process.env.SYNOVEO_CLIENT_SECRET,

  // Optional: Token lifecycle hooks
  hooks: {
    onTokenRefresh: (token) => {
      console.log('Token refreshed:', token.expiresAt)
    }
  }
})

// Tokens are managed automatically
const { locations } = await client.locations.list()

See SDK Documentation for complete details.

On this page