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
| Property | Value |
|---|---|
| Name | proign_token |
| Domain | .proign.com (shared across subdomains) |
| HttpOnly | Yes (not accessible via JavaScript) |
| Secure | Yes (HTTPS only) |
| SameSite | Lax |
| Max-Age | 86400 (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
- Go to Tenant Settings → API Keys
- Click Create API Key
- Select the modules this key can access
- Set an expiration (optional)
- 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
| Prefix | Environment | Example |
|---|---|---|
pk_live_ | Production | pk_live_a1b2c3d4e5f6... |
pk_test_ | Testing/Sandbox | pk_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
| Role | Permissions |
|---|---|
| owner | Full access: billing, users, all modules, API keys |
| admin | Manage users, configure modules, view analytics |
| operator | Day-to-day operations within assigned modules only |
| warehouse-device | Fulfillment 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
| Status | Meaning | Action |
|---|---|---|
401 | Missing or invalid token/key | Check credentials, re-authenticate |
403 | Valid auth but no access | Check tenant/module permissions |
429 | Rate limit exceeded | Wait 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 Type | Rate | Purpose |
|---|---|---|
| Sustained | 1,000 requests/minute | Overall throughput limit |
| Burst | 100 requests/10 seconds | Prevents 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
}| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the limit resets |
Retry-After | Seconds to wait before retrying (on 429) |
Other Endpoint Limits
| Endpoint Type | Limit |
|---|---|
| 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 creation | 5/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')