Getting Started
Quote Plus is a REST API to create and manage quotes simply and securely. Create quotes, share unique links, automatically generate PDFs, and receive responses via webhooks.
Quick Start
- Create an account in the dashboard and get your API Key or JWT Token
- Make a request to create your first quote
- Share the link returned with your client
- Receive notifications when the quote is accepted or rejected
Base URL
API Response Format
When creating a quote, you'll receive a response with the quote details and URLs:
{
"id": "q_01JABCD123",
"status": "PENDING",
"publicUrl": "https://quote.plus/q/abc123xyz",
"pdfUrl": "http://localhost:9000/quotes/pdfs/quote-abc123xyz.pdf"
}publicUrl: Link público para visualizar a quote
pdfUrl: Link para download do PDF (opcional - só aparece se PDF gerado com sucesso)
Authentication
Option 1: JWT Token (Recommended)
Use JWT tokens from Supabase Auth for more secure authentication.
Authorization: Bearer <your-jwt-token>Get the token via POST /api/auth/service-token
Option 2: API Key
Use your API Key for authentication (better for automated scripts).
Authorization: Bearer qp_your_api_key_hereRate Limit: 100 requests per 15 minutes per user
Examples & Use Cases
Basic Use Cases
Basic Quote
Simple creation of a public quote
{
"customer": {
"name": "John Doe",
"email": "[email protected]"
},
"items": [
{
"description": "Website Development",
"qty": 1,
"unitPrice": 5000
}
],
"currency": "USD",
"visibility": "PUBLIC"
}Quote with Total Provided
When you already know the total and don't need to calculate per item
{
"customer": {
"name": "Jane Smith",
"email": "[email protected]"
},
"items": [
{
"description": "Complete Consulting Service",
"qty": 1
}
],
"currency": "USD",
"total": 2500,
"description": "Complete technical consulting service",
"visibility": "PUBLIC"
}Private Quote with Password
Password-protected quote
{
"customer": {
"name": "Bob Johnson",
"email": "[email protected]"
},
"items": [
{
"description": "Confidential Project",
"qty": 1,
"unitPrice": 10000
}
],
"currency": "USD",
"visibility": "PRIVATE",
"password": "secure123",
"expiresAt": "2025-12-31T23:59:59Z"
}Advanced Use Cases
Quote with Discounts and Taxes
Items with percentage discounts and taxes
{
"customer": {
"name": "Alice Williams",
"email": "[email protected]",
"phone": "+1 555 123-4567"
},
"items": [
{
"description": "Mobile App Development",
"qty": 1,
"unitPrice": 15000,
"discount": {
"type": "PERCENT",
"value": 10
},
"taxes": [
{
"name": "Sales Tax",
"type": "PERCENT",
"value": 5
}
]
},
{
"description": "Annual Hosting",
"qty": 1,
"unitPrice": 1200,
"discount": {
"type": "FIXED",
"value": 200
}
}
],
"currency": "USD",
"description": "Complete mobile development project",
"visibility": "PUBLIC"
}Quote with Installments
Payment divided into installments with due dates
{
"customer": {
"name": "Charlie Brown",
"email": "[email protected]"
},
"items": [
{
"description": "Digital Marketing Service",
"qty": 1,
"unitPrice": 8000
}
],
"currency": "USD",
"total": 8000,
"paymentTerms": "Net 30 - Payment in 2 installments",
"installments": [
{
"number": 1,
"amount": 4000,
"dueDate": "2025-01-15T00:00:00Z",
"paymentMethod": "WIRE"
},
{
"number": 2,
"amount": 4000,
"dueDate": "2025-02-15T00:00:00Z",
"paymentMethod": "WIRE"
}
],
"visibility": "PUBLIC"
}Quote with Attachments and Item Details
Quote with attachment links and items with notes/sub-items
{
"customer": {
"name": "ABC Corporation",
"email": "[email protected]"
},
"items": [
{
"description": "Website Redesign Package",
"qty": 1,
"unitPrice": 12000,
"notes": "Includes responsive design, SEO optimization, and 3 months of support",
"subItems": [
{
"description": "Homepage Design",
"qty": 1,
"notes": "Custom design with animations"
},
{
"description": "Product Pages",
"qty": 10,
"notes": "Template-based with customization"
},
{
"description": "Mobile Optimization",
"qty": 1
}
]
},
{
"description": "Content Management System",
"qty": 1,
"unitPrice": 5000,
"notes": "Custom CMS with user-friendly admin panel"
}
],
"currency": "USD",
"description": "Complete website redesign and CMS implementation",
"attachments": [
{
"url": "https://example.com/proposals/website-redesign-proposal.pdf",
"name": "Detailed Proposal PDF",
"type": "PDF"
},
{
"url": "https://example.com/mockups/homepage-design.png",
"name": "Homepage Mockup",
"type": "IMAGE"
},
{
"url": "https://example.com/docs/technical-specs.pdf",
"name": "Technical Specifications",
"type": "PDF"
}
],
"visibility": "PUBLIC"
}Complete Quote with All Fields
Complete example using all available features
{
"customer": {
"name": "XYZ Company Inc",
"email": "[email protected]",
"phone": "+1 555 333-4444"
},
"items": [
{
"description": "ERP System Development",
"qty": 1,
"unitPrice": 50000,
"notes": "Includes custom modules for inventory, accounting, and HR",
"subItems": [
{
"description": "Inventory Management Module",
"qty": 1
},
{
"description": "Accounting Module",
"qty": 1
},
{
"description": "HR Management Module",
"qty": 1
}
],
"discount": {
"type": "PERCENT",
"value": 15
},
"taxes": [
{
"name": "Sales Tax",
"type": "PERCENT",
"value": 5
}
],
"metadata": {
"category": "development",
"priority": "high"
}
},
{
"description": "Team Training",
"qty": 3,
"unitPrice": 2000,
"notes": "3-day intensive training program for your team"
}
],
"currency": "USD",
"description": "Complete custom ERP system development and implementation project",
"attachments": [
{
"url": "https://example.com/docs/erp-proposal.pdf",
"name": "ERP Proposal Document",
"type": "PDF"
},
{
"url": "https://example.com/images/system-architecture.png",
"name": "System Architecture Diagram",
"type": "IMAGE"
}
],
"subtotal": 56000,
"discountTotal": 7500,
"taxTotal": 2425,
"serviceFee": 500,
"total": 51425,
"expiresAt": "2025-12-31T23:59:59Z",
"visibility": "PRIVATE",
"password": "confidential123",
"webhookUrl": "https://mysystem.com/webhooks/quote-plus",
"paymentTerms": "50% on signing, 50% on delivery",
"installments": [
{
"number": 1,
"amount": 25712.5,
"dueDate": "2025-01-01T00:00:00Z",
"paymentMethod": "WIRE"
},
{
"number": 2,
"amount": 25712.5,
"dueDate": "2025-06-01T00:00:00Z",
"paymentMethod": "WIRE"
}
],
"tags": [
"urgent",
"enterprise",
"development"
],
"metadata": {
"projectId": "PROJ-2025-001",
"salesRep": "John Doe"
},
"internalReference": "REF-2025-001"
}Field Reference
PDF Generation & Storage
Automatic PDF Generation
When creating a quote via POST /api/quotes, the system automatically:
- Generates a professional PDF with all quote information
- Stores the PDF securely
- Returns the PDF URL in the response
Response with PDF URL
The response includes a pdfUrl field:
{
"id": "q_01JABCD123",
"status": "PENDING",
"publicUrl": "https://quote.plus/q/abc123xyz",
"pdfUrl": "http://localhost:9000/quotes/pdfs/quote-abc123xyz.pdf"
}Note: The pdfUrl field is optional and only included if PDF generation is successful. If PDF generation fails, the quote is still created successfully, but without the pdfUrl field.
PDF Features
The generated PDF includes:
- Company logo and information (if configured)
- Quote details (number, date, status, reference)
- Customer information
- Complete item list with descriptions, quantities, and prices
- Item notes and sub-items
- Discounts and taxes breakdown
- Payment terms and installments
- Attachments list
- Professional formatting ready for printing
Manual PDF Download
You can also download the PDF directly from the quote page or via API:
Returns the PDF file directly for download.
Example: Using PDF URL
const response = await fetch("https://quote.plus/api/quotes", {
method: "POST",
headers: {
Authorization: "Bearer qp_SUA_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
customer: { name: "John Doe", email: "[email protected]" },
items: [{ description: "Service", qty: 1, unitPrice: 1000 }],
currency: "USD",
}),
});
const quote = await response.json();
// Quote URL for viewing
console.log("View quote:", quote.publicUrl);
// PDF URL for download (if available)
if (quote.pdfUrl) {
console.log("Download PDF:", quote.pdfUrl);
// You can use this URL directly or download it
}Webhooks
Configuration
Configure your webhook in the dashboard to receive notifications when a quote is accepted or rejected.
- Access the dashboard and go to Settings
- Configure the webhook URL
- Choose the events (ACCEPTED, REJECTED)
- Optionally, configure a Webhook Key for additional security
Headers Sent
Content-Type: application/jsonX-QuotePlus-Signature: <HMAC-SHA256-hex>X-QuotePlus-Event: QUOTE_ACCEPTED | QUOTE_REJECTEDX-QuotePlus-Key: <your-webhook-key> (if configured - for identification only)Signature Validation (Important!)
Key vs Secret - Understanding the Difference
- Webhook Secret: Used to calculate the HMAC-SHA256 signature. Copy this from Dashboard → Settings and configure it in your server as an environment variable.
- Webhook Key: Optional. Sent in the X-QuotePlus-Key header for identification purposes. Do NOT use this for signature validation!
How the Signature is Calculated
QuotePlus calculates the signature using HMAC-SHA256:
// QuotePlus calculates the signature like this:
const signature = crypto
.createHmac('sha256', WEBHOOK_SECRET) // Uses your Webhook SECRET
.update(JSON.stringify(payload)) // The JSON body as a string
.digest('hex'); // Returns hexadecimalValidating in Your Server (Node.js/TypeScript)
import crypto from 'crypto';
// Get your Webhook Secret from Dashboard → Settings
const WEBHOOK_SECRET = process.env.QUOTE_PLUS_WEBHOOK_SECRET;
function validateWebhook(rawBody: string | Buffer, signature: string): boolean {
// Calculate expected signature using YOUR Webhook Secret
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody) // IMPORTANT: Use the RAW body, not parsed JSON
.digest('hex');
// Compare signatures securely (timing-safe)
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
} catch {
return false;
}
}
// Usage in Express/NestJS:
// 1. Configure raw body middleware BEFORE json parsing
// 2. Get signature from header: req.headers['x-quoteplus-signature']
// 3. Validate: validateWebhook(rawBody, signature)⚠️ Common Mistakes
- Using Webhook Key instead of Webhook Secret for validation
- Using parsed JSON body instead of raw body string
- Different JSON serialization (extra spaces, different key order)
- Not configuring QUOTE_PLUS_WEBHOOK_SECRET in your .env
Payload Example
{
"event": "QUOTE_ACCEPTED",
"quoteId": "q_01JABCD123",
"publicId": "abc123xyz",
"status": "ACCEPTED",
"customer": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+1-555-123-4567"
},
"response": {
"actorName": "John Doe",
"comment": "Accepted! We can start.",
"answeredAt": "2025-12-03T15:30:00Z"
},
"metadata": {
"externalQuoteId": "your-system-uuid",
"customerId": "cust_123",
"generateWorkOrder": true
},
"internalReference": "REF-2025-001",
"totals": {
"subtotal": 1000,
"discountTotal": 0,
"taxTotal": 80,
"serviceFee": 0,
"total": 1080,
"currency": "USD"
},
"originalPayload": {
"customer": {
"name": "John Doe"
},
"items": [
{
"description": "Service",
"qty": 1,
"unitPrice": 1000
}
],
"currency": "USD"
}
}Payload Fields
| Field | Description |
|---|---|
| metadata | Your custom metadata from the quote - useful for external system IDs (externalQuoteId, customerId, etc.) |
| internalReference | Your internal reference from the quote |
| totals | Calculated totals: subtotal, discountTotal, taxTotal, serviceFee, total, currency |
| originalPayload | Complete original quote payload |