Skip to main content

Authentication

PROIGN supports multiple authentication methods depending on your integration type.

Choose your method: Use JWT Cookies for browser apps, API Keys for server-to-server integrations, or Public Endpoints for customer-facing forms.

1. JWT Cookie Authentication (Browser Apps)

For browser-based applications, authentication uses secure httpOnly cookies:

Login Flow

# 1. User logs in
POST https://www.proign.com/api/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "your-password"
}

# Response sets cookie automatically
Set-Cookie: proign_token=<jwt>; Domain=.proign.com; HttpOnly; Secure; SameSite=Lax

# 2. Subsequent requests include cookie automatically
GET https://app.proign.com/canik/fulfillment/api/orders
Cookie: proign_token=<jwt>

JWT Token Structure

{
  "sub": "user_id_here",
  "email": "user@example.com",
  "name": "John Doe",
  "tenants": [
    {
      "id": "tenant_id",
      "slug": "canik",
      "name": "Canik USA",
      "role": "admin",
      "modules": ["fulfillment", "rewards", "support", "helpdesk"],
      "userModules": ["fulfillment", "rewards"]
    }
  ],
  "iat": 1704067200,
  "exp": 1704153600,
  "iss": "proign.com",
  "aud": "proign-platform"
}

Cookie Properties

PropertyValue
Nameproign_token
Domain.proign.com (shared across subdomains)
HttpOnlyYes (not accessible via JavaScript)
SecureYes (HTTPS only)
SameSiteLax
Max-Age86400 (24 hours)

2. API Key Authentication (Server-to-Server)

For external system integrations (e.g., canik.com, counter.canik.com, Shopify), use API keys:

Getting an API Key

  1. Go to Tenant SettingsAPI Keys
  2. Click Create API Key
  3. Select the modules this key can access
  4. Set an expiration (optional)
  5. Copy the key - it will only be shown once

Using API Keys

# Include the API key in the X-API-Key header
curl -X GET "https://app.proign.com/canik/fulfillment/api/orders" \
  -H "X-API-Key: pk_live_abc123..." \
  -H "Content-Type: application/json"

# Or use Bearer token format
curl -X GET "https://app.proign.com/canik/fulfillment/api/orders" \
  -H "Authorization: Bearer pk_live_abc123..." \
  -H "Content-Type: application/json"

API Key Format

PrefixEnvironmentExample
pk_live_Productionpk_live_a1b2c3d4e5f6...
pk_test_Testing/Sandboxpk_test_x9y8z7w6v5...

Security: Never expose API keys in client-side code, Git repositories, or logs. Store them in environment variables and rotate them regularly.

3. Public Endpoints (No Auth Required)

Some endpoints are publicly accessible for customer-facing functionality:

# Create support ticket (public)
POST https://www.proign.com/api/tenants/canik/support/public/tickets
Content-Type: application/json

{
  "email": "customer@example.com",
  "name": "John Customer",
  "subject": "Product inquiry",
  "description": "I have a question about...",
  "turnstile_token": "<cloudflare-turnstile-token>"
}

# Response includes access token for tracking
{
  "ticket_number": "TKT-2024-001234",
  "access_token": "guest_abc123...",
  "message": "Your support ticket has been created."
}

Public endpoints require Cloudflare Turnstile verification to prevent abuse.

User Roles & Permissions

RolePermissions
ownerFull access: billing, users, all modules, API keys
adminManage users, configure modules, view analytics
operatorDay-to-day operations within assigned modules only
warehouse-deviceFulfillment scanning only (pick, pack, ship)

Module Access Control

Access requires both tenant module enablement AND user permission:

// JWT payload shows both:
{
  "tenants": [{
    "slug": "canik",
    "modules": ["fulfillment", "rewards", "support"],  // Tenant has these
    "userModules": ["fulfillment"]                      // User can access these
  }]
}

// User can only access: fulfillment (intersection of both lists)

Error Responses

StatusMeaningAction
401Missing or invalid token/keyCheck credentials, re-authenticate
403Valid auth but no accessCheck tenant/module permissions
429Rate limit exceededWait and retry (see Retry-After header)

Rate Limits

API Key Limits

API keys have two-tier rate limiting to prevent abuse while allowing burst traffic:

Limit TypeRatePurpose
Sustained1,000 requests/minuteOverall throughput limit
Burst100 requests/10 secondsPrevents rapid-fire requests

Rate Limit Headers

API responses include headers to help you track your rate limit status:

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1704067260

# When rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067260

{
  "error": "Rate limit exceeded. Please try again later.",
  "retryAfter": 45
}
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the limit resets
Retry-AfterSeconds to wait before retrying (on 429)

Other Endpoint Limits

Endpoint TypeLimit
Standard GET (JWT auth)100/minute per user
Write operations (POST/PUT/PATCH)30/minute per user
Auth endpoints (login/register)5/minute per IP
Public ticket creation5/hour per IP per tenant

Integration Examples

cURL

# List orders with API key
curl -X GET "https://app.proign.com/canik/fulfillment/api/orders?status=pending" \
  -H "X-API-Key: pk_live_your_key_here" \
  -H "Content-Type: application/json"

# Create support ticket
curl -X POST "https://www.proign.com/api/tenants/canik/support/public/tickets" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "customer@example.com",
    "name": "Customer Name",
    "subject": "Help needed",
    "description": "Details here..."
  }'

JavaScript/TypeScript

// Server-side with API key and rate limit handling
async function fetchWithRateLimit(url: string) {
  const response = await fetch(url, {
    headers: {
      'X-API-Key': process.env.PROIGN_API_KEY!,
      'Content-Type': 'application/json',
    },
  });

  // Log rate limit status
  const remaining = response.headers.get('X-RateLimit-Remaining');
  const limit = response.headers.get('X-RateLimit-Limit');
  console.log(`Rate limit: ${remaining}/${limit} remaining`);

  // Handle rate limiting
  if (response.status === 429) {
    const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
    console.log(`Rate limited. Retrying in ${retryAfter}s...`);
    await new Promise(r => setTimeout(r, retryAfter * 1000));
    return fetchWithRateLimit(url); // Retry
  }

  return response.json();
}

const { data, meta } = await fetchWithRateLimit(
  'https://app.proign.com/canik/fulfillment/api/orders'
);

// Browser with cookies (automatic)
const response = await fetch('/api/canik/orders', {
  credentials: 'include',
});

Python

import requests
import os
import time

def fetch_with_rate_limit(url: str, max_retries: int = 3):
    """Fetch with automatic rate limit handling."""
    for attempt in range(max_retries):
        response = requests.get(
            url,
            headers={
                'X-API-Key': os.environ['PROIGN_API_KEY'],
                'Content-Type': 'application/json',
            }
        )

        # Log rate limit status
        remaining = response.headers.get('X-RateLimit-Remaining', 'N/A')
        limit = response.headers.get('X-RateLimit-Limit', 'N/A')
        print(f"Rate limit: {remaining}/{limit} remaining")

        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            print(f"Rate limited. Waiting {retry_after}s...")
            time.sleep(retry_after)
            continue

        response.raise_for_status()
        return response.json()

    raise Exception("Max retries exceeded")

data = fetch_with_rate_limit('https://app.proign.com/canik/fulfillment/api/orders')

Related