Overview
VIQ exposes a RESTful API over HTTPS. All requests and responses use JSON. The API is divided into two layers: the Cloudflare Worker endpoints (authentication, usage metering, email, Stripe) and the Table API (CRUD operations on your data).
Early Access API. The VIQ API is currently in early access for Growth, Franchisor, and Enterprise plan customers. Partner integrations and white-label access are available — contact hello@vendorintel.ai.
⚡
Cloudflare Edge
Worker runs at the edge globally — sub-50ms typical response
🔐
JWT Auth
HMAC-SHA256 signed tokens, 30-day expiry, server-verified
💳
Stripe Webhooks
HMAC-verified — tier upgrades applied server-side only
📊
REST + JSON
Standard REST verbs: GET, POST, PUT, PATCH, DELETE
Authentication
VIQ uses JWT bearer tokens for API authentication. Tokens are issued by the
/issue-token Worker endpoint on every login.Pass your JWT in the Authorization header as a Bearer token:
Authorization: Bearer <your_token>Example Request Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
JWT Payload Structure
{
"sub": "user_abc123", // user ID
"email": "user@company.com",
"name": "Jane Smith",
"role": "franchisee",
"tier": "growth",
"company": "Sunrise Café Group",
"iat": 1722470400, // issued at (Unix timestamp)
"exp": 1725062400 // expires at (Unix timestamp, 30 days)
}
Base URL
All API requests use relative paths when called from the VIQ app, or the full Worker URL for external integrations.
Worker Base URL (external integrations)
https://viq-qb-proxy.danielle-f99.workers.dev
Table API Base URL
https://viq.vendorintel.ai/tables/{table_name}
The Worker enforces CORS — requests from external origins require pre-approval. Contact hello@vendorintel.ai to whitelist your domain.
Rate Limits
The API is rate-limited at the Cloudflare edge.
| Tier | Requests / minute | Notes |
|---|---|---|
| Free | 30 rpm | Table API only |
| Essentials / Growth | 120 rpm | Worker + Table API |
| Franchisor / Pro | 300 rpm | Full access |
| Enterprise | Custom | Contact us |
Rate limit headers are returned on all responses: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
POST /issue-token
Issues a signed JWT for an authenticated user. Called automatically on every login. Returns a 30-day token.
Request
POST https://viq-qb-proxy.danielle-f99.workers.dev/issue-token
Content-Type: application/json
{
"userId": "user_abc123",
"email": "user@company.com",
"name": "Jane Smith",
"role": "franchisee",
"tier": "growth",
"company": "Sunrise Café Group"
}
Response 200
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": 1725062400
}
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Required | User's unique ID (UUID from viq_users) |
| string | Required | User's email address | |
| role | string | Required | business | franchisee | franchisor | vendor | admin |
| tier | string | Required | free | essentials | growth | franchisor_s | franchisor_m | … |
| company | string | Optional | Company / brand name for display |
POST /validate-token
Validates a JWT and returns its decoded payload. Use this to verify tokens in server-side integrations.
Request
POST /validate-token
Content-Type: application/json
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
Response 200
{
"valid": true,
"payload": { "sub": "user_abc123", "email": "...", "tier": "growth", "exp": 1725062400 }
}
Response 401 (invalid / expired)
{ "valid": false, "error": "Token expired" }POST /check-limit
Server-side usage metering. Returns 200 if the user is within their tier's vendor cap, or 402 if they've exceeded it. Used to gate data writes at the Worker level.
Request
POST /check-limit
Authorization: Bearer <jwt>
Content-Type: application/json
{ "userId": "user_abc123", "currentCount": 4 }
Response 200 (within limit)
{ "allowed": true, "tier": "growth", "cap": 100, "used": 4 }Response 402 (over limit)
{ "allowed": false, "tier": "free", "cap": 3, "used": 4, "upgrade_url": "https://viq.vendorintel.ai/upgrade.html" }Table API — Overview
The Table API provides full CRUD access to VIQ's data tables. All responses are paginated JSON. System fields (
id, created_at, updated_at) are managed automatically.| Table | Description |
|---|---|
| viq_users | User accounts — 27 fields including tier, role, session_count, features_used |
| viq_vendors | Vendor records — VIQ scores, benchmarks, share_public flag, share_view_count |
| viq_submissions | Scoring submissions — raw inputs and scored outputs |
| viq_team_members | Multi-seat invites — owner/admin/analyst/read_only roles |
| viq_onboarding_progress | Activation checklist state — 4 steps, drip flags |
| viq_payments | Payment log — written by Stripe webhook (idempotency record) |
| viq_accountant_clients | Accountant ↔ client linkage for white-label access |
| viq_support_tickets | Support tickets |
GET /tables/{table}
List records with pagination and optional search/sort.
Request
GET /tables/viq_vendors?page=1&limit=20&search=Toast&sort=created_at Authorization: Bearer <jwt>
Response 200
{
"data": [ { "id": "uuid", "vendor_name": "Toast POS", "viq_score": 72, ... } ],
"total": 47,
"page": 1,
"limit": 20,
"table": "viq_vendors",
"schema": { ... }
}
| Query Param | Type | Description | |
|---|---|---|---|
| page | number | Optional | Page number, default 1 |
| limit | number | Optional | Records per page, default 100, max 500 |
| search | string | Optional | Full-text search across all text fields |
| sort | string | Optional | Field name to sort by (default: updated_at DESC) |
POST /tables/{table}
Create a new record. Returns HTTP 201 with the created record including system fields.
JavaScript Example
const response = await fetch('tables/viq_vendors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: 'user_abc123',
vendor_name: 'Toast POS',
vendor_category: 'Point of Sale',
monthly_spend: 1450,
viq_score: 72,
viq_status: 'watch',
share_public: false
})
});
const vendor = await response.json();
console.log(vendor.id); // → "550e8400-e29b-41d4-a716-446655440000"
PUT / PATCH /tables/{table}/{id}
Update a record. PUT replaces all fields; PATCH updates only the fields provided.
PATCH Example — update score only
PATCH /tables/viq_vendors/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{ "viq_score": 85, "viq_status": "safe" }
Webhooks — Stripe Events
VIQ's Worker receives and processes Stripe webhook events. Signatures are verified with HMAC-SHA256 before any action is taken.
Webhook endpoint:
https://viq-qb-proxy.danielle-f99.workers.dev/stripe-webhookcheckout.session.completed
Fired when a Stripe Checkout session completes. Upgrades the user's tier, logs to viq_payments, sends confirmation email.
checkout.session.async_payment_succeeded
Fired for delayed payment methods. Same flow as checkout.session.completed.
customer.subscription.deleted
Fired when a subscription is cancelled. Downgrades user tier to 'free'. (Planned — not yet in Worker.)
Stripe Event Payload (checkout.session.completed)
{
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_abc123",
"customer_email": "user@company.com",
"client_reference_id": "user_abc123_growth",
"metadata": { "plan": "growth" },
"payment_status": "paid"
}
}
}
Tier Caps Reference
Vendor record limits enforced by
/check-limit.| Tier | Vendor Cap | Team Seats |
|---|---|---|
| free | 3 | 1 (owner only) |
| essentials | 20 | 1 |
| growth | 100 | 5 |
| franchisor_s | 10 locations | 10 |
| franchisor_m | 50 locations | 25 |
| franchisor_l | Unlimited | 100 |
| accountant_starter | 5 clients | 3 |
| accountant_pro | 20 clients | 10 |
| accountant_enterprise | Unlimited | 50 |
| vendor_intel / vendor_pro | Unlimited | 3–5 |
| admin | Unlimited | Unlimited |
System Fields
Automatically managed on every record — never set these manually.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique record identifier — UUID v4 |
| gs_project_id | string | Project identifier (internal) |
| gs_table_name | string | Table name (internal) |
| created_at | number (ms) | Creation timestamp in milliseconds |
| updated_at | number (ms) | Last update timestamp in milliseconds |
Error Codes
All errors return JSON with an
error field.| HTTP Status | Meaning | Common Cause |
|---|---|---|
| 200 | OK | Success |
| 201 | Created | Record created (POST) |
| 204 | No Content | Record deleted (DELETE) |
| 400 | Bad Request | Missing required fields or invalid JSON |
| 401 | Unauthorized | Missing, invalid, or expired JWT |
| 402 | Payment Required | Tier vendor cap exceeded (/check-limit) |
| 403 | Forbidden | Insufficient role or tier for this action |
| 404 | Not Found | Record ID does not exist |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Worker exception — contact support |
Error Response Format
{ "error": "Token expired", "code": 401 }