REST API · v1
API Reference
Quote lifecycle
Every API call either reads or advances a quote through this lifecycle. Authentication is tenant-scoped; all monetary values are DECIMAL(14,4). All routes are served under the /v1 prefix (locally, http://localhost:3001/v1).
https://api.tackquote.com/v1
Authentication
All requests require a Bearer token obtained from the login endpoint. Each token is scoped to a single tenant — the tenantSlug in your login request determines which data the token can access.
POST /v1/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "your-password",
"tenantSlug": "your-tenant"
}{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900
}Include the access token on every subsequent request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...expiresIn: 900). Refresh before expiry with POST /v1/auth/refresh, sending { "refreshToken": "..." } in the body. Refresh tokens last 7 days. New accounts are created via POST /v1/auth/register followed by POST /v1/auth/verify-email.API Keys
For server-to-server integrations, issue a long-lived API key instead of logging in with a password. Manage keys under /v1/api-keys (or in the seller portal). The key limit depends on your plan — see Plans & Billing.
/v1/api-keys/v1/api-keys/v1/api-keys/:idPOST /v1/api-keys
Authorization: Bearer eyJ...
Content-Type: application/json
{
"name": "Warehouse sync",
"scopes": ["quotes:read", "buyers:read"]
}
// Response 201 — the plaintext key is returned ONCE
{
"id": "9c1e...",
"key": "tack_sk_a1b2c3d4e5f6...",
"scopes": ["quotes:read", "buyers:read"]
}tack_sk_ and shown in full only at creation — TackQuote stores just a bcrypt hash. If you lose a key, revoke it and issue a new one.Rate Limits
A platform baseline of 100 requests per 60 seconds applies to every tenant, layered with a per-plan per-minute cap — 60 (Basic), 120 (Starter), 300 (Pro), and 1,000 (Enterprise) requests per minute. Plans also carry a monthly API-call allowance; see Plans & Billing. Requests over any limit return 429 Too Many Requests. Response headers tell you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Max requests per minute |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | UTC epoch timestamp when the window resets |
Quotes API
The core resource. Quotes move through the lifecycle shown above — every state transition writes to an immutable event log. All monetary fields are DECIMAL(14,4).
/v1/quotes?page=1&limit=20&status=sent/v1/quotes/v1/quotes/:id/v1/quotes/:id/v1/quotes/:id/sendList quotes
Returns paginated results. Filter by status, buyerId, or search (matches quote number and title).
GET /v1/quotes?page=1&limit=20&status=sent
Authorization: Bearer eyJ...
// Response
{
"data": [ /* Quote[] */ ],
"total": 84,
"page": 1,
"limit": 20
}Create quote
POST /v1/quotes
Authorization: Bearer eyJ...
Content-Type: application/json
{
"buyerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Annual SaaS Renewal — Acme Corp",
"lineItems": [
{
"description": "Pro Plan (annual)",
"sku": "PRO-ANN-001",
"quantity": 10,
"unitPrice": 1200.0000,
"discount": 0.10
}
],
"expiresAt": "2026-07-20T23:59:59Z",
"notes": "Volume discount applied."
}Send quote
Transitions the quote from draft or pending_approval to sent and delivers an email to the buyer. Returns the updated quote object.
POST /v1/quotes/3fa85f64-5717-4562-b3fc-2c963f66afa6/send
Authorization: Bearer eyJ...Quote object — key fields
| Field | Type | Description |
|---|---|---|
id | uuid | Unique identifier |
tenantId | uuid | Your tenant |
quoteNumber | string | Human-readable — e.g. TK-2026-000142 |
title | string | Quote title shown to buyer |
status | enum | draft · sent · viewed · approved · rejected · expired · converted |
buyerId | uuid | Linked buyer record |
lineItems | LineItem[] | {description, sku, quantity, unitPrice, discount} |
subtotal | decimal | Sum before discount and tax |
discount | decimal | Total discount amount |
tax | decimal | Tax amount |
total | decimal | Final amount payable |
expiresAt | timestamptz | Quote expiry |
createdAt | timestamptz | Creation timestamp |
Buyers API
Buyers are the contacts and companies you quote. Each buyer belongs to your tenant and can be linked to multiple quotes across their lifetime.
/v1/buyers/v1/buyers/v1/buyers/:id/v1/buyers/:idCreate buyer
POST /v1/buyers
Authorization: Bearer eyJ...
Content-Type: application/json
{
"name": "Jane Doe",
"email": "[email protected]",
"company": "Acme Corp",
"phone": "+1-555-0100" // optional
}Products / Catalog
The product catalog backs the line-item picker in the quote builder. Products sync automatically from connected BigCommerce or Shopify stores, and can also be managed directly via API.
/v1/catalog/products?search=term/v1/catalog/productsCreate product
POST /v1/catalog/products
Authorization: Bearer eyJ...
Content-Type: application/json
{
"name": "Pro Plan — Annual",
"sku": "PRO-ANN-001",
"description": "Unlimited users, SSO, priority support.",
"unitPrice": 1200.0000,
"category": "Subscriptions"
}Webhooks
Subscribe to quote lifecycle events. TackQuote posts to your URL within seconds of each state change — use this to trigger CRM updates, notify Slack, or start downstream workflows.
Register a webhook
POST /v1/webhooks
Authorization: Bearer eyJ...
Content-Type: application/json
{
"url": "https://your-app.com/tack/webhook",
"secret": "whsec_at_least_16_chars",
"events": [
"quote.sent",
"quote.viewed",
"quote.approved",
"quote.rejected",
"quote.converted"
]
}Event payload
{
"event": "quote.converted",
"timestamp": "2026-06-20T12:00:00Z",
"data": {
"quoteId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"quoteNumber": "TK-2026-000142",
"buyerId": "b1c2d3e4-1234-5678-abcd-ef0123456789"
}
}Available events
X-TackQuote-Signature header of the form sha256=<hex> — the HMAC-SHA256 of the raw body using the secret you set when registering the webhook. The event name is also sent in X-TackQuote-Event. Reject requests where the signature doesn't match.Signature verification — Node.js
import crypto from 'crypto'
// header = req.headers['x-tack-signature'] // "sha256=<hex>"
function verifyTackWebhook(
rawBody: string,
header: string,
secret: string
): boolean {
const signature = header.replace(/^sha256=/, '')
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
)
}Error Codes
All errors return JSON with a message field. Validation errors include an errors array with per-field detail.
| Code | Status | Meaning |
|---|---|---|
| 400 | Bad Request | The request body is malformed or missing required fields. |
| 401 | Unauthorized | No Bearer token provided, or the token has expired. |
| 403 | Forbidden | Your token doesn't have permission to access this resource. |
| 404 | Not Found | The resource does not exist. |
| 422 | Validation Error | Well-formed request that fails field-level validation. Check the errors array. |
| 429 | Rate Limit | You exceeded the platform (100/60s) or your per-plan per-minute request limit. |
| 500 | Server Error | Something went wrong on our end. If it repeats, contact support. |
{
"statusCode": 422,
"message": "Validation failed",
"errors": [
{ "field": "lineItems[0].unitPrice", "message": "Must be greater than 0" },
{ "field": "expiresAt", "message": "Must be a future date" }
]
}SDKs & Tools
Interactive OpenAPI explorer at /api/docs in non-production environments (e.g. localhost:3001/api/docs).
Pre-built collection for all endpoints, ready to import.
Type-safe client with full IntelliSense. Built on tRPC v11.