Skip to content

Authentication

HappyColis uses OAuth 2.0 for API authentication. This guide covers all grant types, token lifecycle, introspection, and security best practices.

Application Types

Private Applications

Designed for a specific organization. Credentials (client_id / client_secret) are generated through the HappyColis dashboard. Use the client_credentials grant type.

Public Applications

Can be installed by any organization. Use the authorization_code grant type (3-legged OAuth) to obtain tokens on behalf of an organization.


OAuth Endpoints

EndpointMethodDescription
/tokenPOST / GETObtain or refresh access tokens
/oauth2/tokenPOSTAlternative token endpoint
/oauth2/meGETToken introspection
/oauth2/grantGETAuthorization page (public apps)

Client Credentials Flow (Server-to-Server)

The recommended flow for private backend integrations. Your server exchanges credentials directly for an access token — no user interaction needed.

Request Format

POST /token
Content-Type: application/x-www-form-urlencoded
ParameterRequiredDescription
grant_typeMust be client_credentials
client_idYour application's client ID
client_secretYour application's client secret
organization_idThe organization UUID to authenticate against
scopesSpace-separated scope list (defaults to all client scopes)

Response Format

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
  "expires_in": 3600,
  "scope": "view_products,create_orders"
}
FieldTypeDescription
access_tokenstringJWT bearer token — include in Authorization header
token_typestringAlways bearer
refresh_tokenstringOpaque token for refreshing; longer-lived (default: 86400s)
expires_inintegerAccess token lifetime in seconds (default: 3600)
scopestringComma-separated list of granted scopes

JWT Claims

The access token is a signed JWT with the following claims:

ClaimDescription
issToken ID
audOrganization ID
subclient://{clientId} for client credentials
jtiToken ID (same as iss)
scopeArray of granted scopes
expExpiry timestamp (Unix seconds)
iatIssued-at timestamp (Unix seconds)
client_idClient application ID

Code Examples

bash
curl -X POST https://api-v3.happycolis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "organization_id=YOUR_ORGANIZATION_ID"
js
async function getAccessToken(clientId, clientSecret, organizationId) {
  const params = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: clientId,
    client_secret: clientSecret,
    organization_id: organizationId,
  });

  const response = await fetch('https://api-v3.happycolis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.message || 'Authentication failed');
  }

  return response.json();
  // { access_token, token_type, refresh_token, expires_in, scope }
}

const tokens = await getAccessToken('client_id', 'client_secret', 'org_uuid');
console.log(tokens.access_token);
python
import requests

def get_access_token(client_id: str, client_secret: str, organization_id: str) -> dict:
    response = requests.post(
        'https://api-v3.happycolis.com/token',
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        data={
            'grant_type': 'client_credentials',
            'client_id': client_id,
            'client_secret': client_secret,
            'organization_id': organization_id,
        },
    )
    response.raise_for_status()
    return response.json()

tokens = get_access_token('client_id', 'client_secret', 'org_uuid')
print(tokens['access_token'])
php
function getAccessToken(string $clientId, string $clientSecret, string $organizationId): array
{
    $client = new \GuzzleHttp\Client();
    $response = $client->post('https://api-v3.happycolis.com/token', [
        'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
        'form_params' => [
            'grant_type'      => 'client_credentials',
            'client_id'       => $clientId,
            'client_secret'   => $clientSecret,
            'organization_id' => $organizationId,
        ],
    ]);
    return json_decode($response->getBody()->getContents(), true);
}

$tokens = getAccessToken('client_id', 'client_secret', 'org_uuid');
echo $tokens['access_token'];
go
func getAccessToken(clientID, clientSecret, orgID string) (map[string]interface{}, error) {
    data := url.Values{
        "grant_type":      {"client_credentials"},
        "client_id":       {clientID},
        "client_secret":   {clientSecret},
        "organization_id": {orgID},
    }
    resp, err := http.PostForm("https://api-v3.happycolis.com/token", data)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result map[string]interface{}
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }
    return result, nil
}

Using the Access Token

Include the token in the Authorization header on every API request:

Authorization: Bearer YOUR_ACCESS_TOKEN

Token Introspection

Verify a token and inspect its claims:

bash
curl -X GET https://api-v3.happycolis.com/oauth2/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

json
{
  "sub": "client://your_client_id",
  "aud": "org_123",
  "scope": ["view_products", "create_orders"],
  "exp": 1699999999,
  "iat": 1699996399,
  "client_id": "your_client_id"
}

The sub field format indicates the token type:

sub prefixToken type
client://Client credentials token
user://User (password grant) token
admin://Admin user token

Refresh Token Flow

Access tokens expire after expires_in seconds (default: 3600). Use the refresh token to obtain a new access token without re-authenticating.

Request

POST /token
Content-Type: application/x-www-form-urlencoded
ParameterRequiredDescription
grant_typerefresh_token
refresh_tokenThe refresh token from a previous response
client_idYour application's client ID
bash
curl -X POST https://api-v3.happycolis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"
js
async function refreshAccessToken(clientId, refreshToken) {
  const params = new URLSearchParams({
    grant_type: 'refresh_token',
    client_id: clientId,
    refresh_token: refreshToken,
  });

  const response = await fetch('https://api-v3.happycolis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.message || 'Token refresh failed');
  }

  return response.json();
}
python
def refresh_access_token(client_id: str, refresh_token: str) -> dict:
    response = requests.post(
        'https://api-v3.happycolis.com/token',
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        data={
            'grant_type': 'refresh_token',
            'client_id': client_id,
            'refresh_token': refresh_token,
        },
    )
    response.raise_for_status()
    return response.json()
php
function refreshToken(string $clientId, string $refreshToken): array
{
    $client = new \GuzzleHttp\Client();
    $response = $client->post('https://api-v3.happycolis.com/token', [
        'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
        'form_params' => [
            'grant_type'    => 'refresh_token',
            'client_id'     => $clientId,
            'refresh_token' => $refreshToken,
        ],
    ]);
    return json_decode($response->getBody()->getContents(), true);
}
go
func refreshToken(clientID, refreshToken string) (map[string]interface{}, error) {
    data := url.Values{
        "grant_type":    {"refresh_token"},
        "client_id":     {clientID},
        "refresh_token": {refreshToken},
    }
    resp, err := http.PostForm("https://api-v3.happycolis.com/token", data)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    return result, nil
}

Authorization Code Flow (Public Applications)

Used for public apps that need to act on behalf of an organization (3-legged OAuth).

Step 1: Redirect to Authorization Page

GET https://api-v3.happycolis.com/oauth2/grant
  ?client_id=YOUR_CLIENT_ID
  &organization=ORGANIZATION_ID
  &scope=view_products,create_orders
  &redirect_uri=https://your-app.com/callback
  &state=random_csrf_token
ParameterRequiredDescription
client_idYour application's client ID
organizationThe organization UUID
scopeComma-separated list of requested scopes
redirect_uriURL to redirect after authorization
stateRecommendedRandom string for CSRF protection

Step 2: Handle the Callback

After the user grants permission, HappyColis redirects to your redirect_uri with:

  • code — Authorization code (short-lived, single-use)
  • hmac — HMAC-SHA256 signature of the query string

Always verify the HMAC signature before processing the code.

Step 3: Exchange Code for Token

bash
curl -X POST https://api-v3.happycolis.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

OAuth Scopes

Request specific scopes during authentication to limit access. Use * for full access (if the client is configured to allow it).

Catalog Scopes

ScopeDescription
view_productsView products and variants
create_productsCreate products and variants
edit_productsUpdate products and variants
view_vendorsView vendors
create_vendorsCreate vendors
edit_vendorsUpdate vendors
view_preparation_profilesView preparation profiles
create_preparation_profilesCreate preparation profiles
edit_preparation_profilesUpdate preparation profiles

Order Scopes

ScopeDescription
view_ordersView orders
create_ordersCreate orders
edit_ordersUpdate, cancel, hold, resume orders and request fulfillment
delete_ordersDelete orders
manage_ordersFull access to orders

Stock Scopes

ScopeDescription
view_stock_referencesView stock references and inventory
create_stock_referencesCreate stock references
edit_stock_referencesAdjust inventory, update stock references
delete_stock_referencesDelete stock references
manage_stock_referencesFull access to stock
view_storage_profilesView storage profiles
create_storage_profilesCreate storage profiles
edit_storage_profilesUpdate storage profiles

Location Scopes

ScopeDescription
view_locationsView locations
create_locationsCreate locations
edit_locationsUpdate locations, connect fulfillment services
delete_locationsDelete locations
manage_locationsFull access to locations

Delivery Order Scopes

ScopeDescription
view_delivery_ordersView delivery orders
create_delivery_ordersCreate delivery orders
edit_delivery_ordersUpdate delivery orders, send fulfillment events
delete_delivery_ordersDelete delivery orders
manage_delivery_ordersFull access to delivery orders

Shipment Scopes

ScopeDescription
view_shipmentsView shipments
create_shipmentsCreate shipments
edit_shipmentsUpdate shipments, tracking events
delete_shipmentsDelete shipments
manage_shipmentsFull access to shipments

Integration & Webhook Scopes

ScopeDescription
view_integrationsView integrations
create_integrationsCreate integrations
edit_integrationsUpdate integrations
view_integrations_webhooksView webhooks
create_integrations_webhooksCreate webhooks
edit_integrations_webhooksUpdate webhooks

Error Responses

All token errors return HTTP 401 with a JSON body:

Invalid Client Credentials

json
{
  "statusCode": 401,
  "message": "Invalid client ID and/or grant type is not allowed"
}

Causes:

  • client_id does not exist
  • client_secret is wrong
  • grant_type is not enabled for this client
  • organization_id does not match the client's organization binding

Invalid Refresh Token

json
{
  "statusCode": 401,
  "message": "Invalid refresh token"
}

Causes:

  • Refresh token does not exist
  • Refresh token has expired (default TTL: 86400s / 24h)

Missing Organization ID

json
{
  "statusCode": 401,
  "message": "Client credentials grant type requires an organization ID"
}

Cause: organization_id was not included in the request body.

Invalid User Credentials (password grant)

json
{
  "statusCode": 401,
  "message": "Invalid user credentials"
}

Grant Type Not Allowed

json
{
  "statusCode": 401,
  "message": "Grant type not allowed"
}

Using an Expired Token

json
{
  "statusCode": 401,
  "message": "Unauthorized"
}

The JWT verification middleware rejects tokens past their exp claim.


Security Best Practices

  1. Keep client_secret server-side only. Never expose it in browser code, mobile apps, or version control.
  2. Use HTTPS exclusively. All API endpoints require TLS.
  3. Implement token refresh proactively. Refresh the access token before it expires rather than waiting for a 401. Check expires_in and refresh ~60 seconds before expiry.
  4. Request minimum scopes. Only request the scopes your integration actually needs. Limiting scope reduces blast radius if credentials are compromised.
  5. Rotate credentials periodically. If you suspect a secret leak, rotate immediately via the dashboard.
  6. Validate HMAC on authorization code callbacks. Always verify the hmac parameter before exchanging the code for a token (authorization code flow).
  7. Use the state parameter in authorization code flow to prevent CSRF attacks. Verify the returned state matches what you sent.
  8. Store tokens securely. Prefer secure, encrypted storage over plain environment variables for production deployments.

HappyColis API Documentation