Developer guide
BTAB Vendor Integration Guide
This guide shows you how to integrate BTAB's product catalog and order management into your own custom storefront.
Written with the code it describes, in the same pull request. Plain Markdown: /developers/vendor-integration-guide.md
Welcome to BTAB!
This guide shows you how to integrate BTAB's product catalog and order management into your own custom storefront.
Overview
What is BTAB? BTAB is a B2B2C platform that lets you sell products without holding inventory. You:
- Select products from our catalog
- Build your own storefront (any tech stack)
- Set your own pricing
- Sell to your customers
- We handle fulfillment
You Focus On: Marketing, branding, customer experience We Handle: Inventory, fulfillment, shipping
Quick Start (5 Steps)
1. Create Your BTAB Account
Visit https://dashboard.btab.app/register
Email: [email protected]
Password: [your secure password]
Company Name: Your Store Inc.
You'll receive an API key that looks like:
btab_live_abc123def456ghi789...
⚠️ KEEP THIS SECRET! Never expose it in browser code.
2. Select Products
- Log into https://dashboard.btab.app
- Go to "Products" tab
- Browse the catalog
- Click "Add to Store" on products you want to sell
- Optionally set custom pricing (your retail price)
Example:
- Wholesale price: $49.99
- Suggested retail: $89.99
- Your custom price: $79.99 (your margin: $30)
3. Set Up Your Backend
CRITICAL: Your API key must ONLY be used from your backend server, NEVER in browser JavaScript.
Why? If you put it in browser code, anyone can:
- Steal your API key using browser devtools
- Make unauthorized requests
- Impersonate your store
- Create fake orders
Correct Architecture:
[Customer Browser]
↓ public requests
[Your Backend Server] ← API key stored here (env variable)
↓ authenticated requests
[BTAB API]
4. Integrate the API
Node.js Example
Install dependencies:
npm install express node-fetch dotenv
Create .env file:
BTAB_API_KEY=btab_live_abc123def456...
BTAB_API_URL=https://api.btab.app/api/v1
PORT=8000
Create server.js:
require('dotenv').config();
const express = require('express');
const app = express();
app.use(express.json());
const BTAB_API_KEY = process.env.BTAB_API_KEY;
const BTAB_API_URL = process.env.BTAB_API_URL;
// Get products for your store
app.get('/api/products', async (req, res) => {
try {
const response = await fetch(`${BTAB_API_URL}/my-products`, {
headers: {
'Authorization': `Bearer ${BTAB_API_KEY}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch products');
}
const data = await response.json();
res.json(data);
} catch (error) {
console.error('Error fetching products:', error);
res.status(500).json({ error: 'Failed to load products' });
}
});
// Handle customer checkout
app.post('/api/checkout', async (req, res) => {
try {
const { customer, items, shipping, payment } = req.body;
// Create order in BTAB
const response = await fetch(`${BTAB_API_URL}/orders`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${BTAB_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
customer,
items,
shipping,
payment
})
});
if (!response.ok) {
throw new Error('Order creation failed');
}
const order = await response.json();
res.json({
success: true,
order_id: order.id,
message: 'Order placed successfully!'
});
} catch (error) {
console.error('Error creating order:', error);
res.status(500).json({ error: 'Checkout failed' });
}
});
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
Start your server:
node server.js
5. Build Your Frontend
Your frontend can be anything:
- React, Vue, Angular
- Next.js, Nuxt
- Plain HTML/JS/CSS
- WordPress
- Mobile app
Example Frontend (React):
import { useState, useEffect } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Call YOUR backend, not BTAB directly
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data.products);
setLoading(false);
})
.catch(error => {
console.error('Error loading products:', error);
setLoading(false);
});
}, []);
if (loading) return <div>Loading products...</div>;
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
function ProductCard({ product }) {
return (
<div className="product-card">
<img src={product.image_url} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="price">
${(product.retail_price_cents / 100).toFixed(2)}
</p>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
);
}
async function checkout(cart, customer, shipping, payment) {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer,
items: cart.items.map(item => ({
product_id: item.product.id,
quantity: item.quantity,
price_cents: item.product.retail_price_cents
})),
shipping,
payment
})
});
return response.json();
}
API Reference
Base URL
https://api.btab.app/api/v1
Authentication
All requests require API key in header:
Authorization: Bearer btab_live_abc123...
Get Your Selected Products
Endpoint: GET /my-products
Response:
{
"products": [
{
"id": 1,
"name": "Product Name",
"description": "Product description...",
"sku": "SKU-001",
"wholesale_price_cents": 4999,
"retail_price_cents": 8999,
"custom_retail_price_cents": 7999,
"image_url": "https://...",
"in_stock": true,
"stock_quantity": 50,
"metadata": {
"category": "electronics",
"weight_oz": 16
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total_count": 25,
"total_pages": 2
}
}
Get Full Product Catalog
Endpoint: GET /products
Shows ALL available products (not just yours). Useful for letting vendors browse and add new products via API.
Query Parameters:
page- Page number (default: 1)limit- Items per page (default: 20, max: 100)search- Search termin_stock- true/false
Add Product to Your Store
Endpoint: POST /my-products
Request Body:
{
"product_id": 1,
"custom_retail_price_cents": 7999
}
Response:
{
"message": "Product added to your store",
"vendor_product": {
"id": 123,
"product_id": 1,
"custom_retail_price_cents": 7999
}
}
Remove Product from Your Store
Endpoint: DELETE /my-products/:id
Response:
{
"message": "Product removed from your store"
}
Update Product Pricing
Endpoint: PUT /api/v1/my-products/{productId}/pricing
Request Body:
{
"custom_retail_price_cents": 8999
}
Create Order
Endpoint: POST /orders
Request Body:
{
"customer": {
"email": "[email protected]",
"name": "John Doe",
"phone": "+1234567890"
},
"items": [
{
"product_id": 1,
"quantity": 2,
"price_cents": 7999
}
],
"shipping": {
"address": "123 Main St",
"address2": "Apt 4B",
"city": "San Francisco",
"state": "CA",
"zip": "94102",
"country": "US"
},
"payment": {
"method": "stripe",
"transaction_id": "ch_abc123",
"amount_cents": 15998
}
}
Response:
{
"order": {
"id": 456,
"order_number": "ORD-2025-001",
"status": "pending",
"total_cents": 15998,
"created_at": "2025-10-16T12:00:00Z"
}
}
Get Your Orders
Endpoint: GET /orders
Query Parameters:
page- Page numberlimit- Items per pagestatus- Filter by status (pending, processing, shipped, delivered, cancelled)
Get Specific Order
Endpoint: GET /orders/:id
Response:
{
"order": {
"id": 456,
"order_number": "ORD-2025-001",
"status": "shipped",
"customer": { ... },
"items": [ ... ],
"shipping": { ... },
"tracking_number": "1Z999AA10123456784",
"created_at": "2025-10-16T12:00:00Z",
"updated_at": "2025-10-16T14:30:00Z"
}
}
Get the Buyer's Tax Invoice
Endpoint: GET /orders/:id/invoice
Returns the buyer's tax invoice for this order as a PDF (Content-Type: application/pdf), not JSON.
BTAB is the merchant of record on every storefront, so BTAB issues the tax
invoice and the document carries BTAB's legal name and ABN — your store appears
on it as the channel the sale came through. The invoice is issued when the
order ships, is emailed to the buyer automatically at that moment, and carries
a permanent number in the form INV-YYYYMM-NNNNNN.
Before the order ships there is no invoice, and this endpoint returns 404 with
a message saying so. The number and issue date are also returned on
GET /orders/:id as invoice_number and invoice_issued_at — check those if
you want to know whether a document exists before requesting it.
GET /orders/456/invoice
Authorization: Bearer <your-api-key>
200 OK
Content-Type: application/pdf
Content-Disposition: inline; filename="INV-202609-000042.pdf"
Error Handling
All errors follow this format:
{
"error": "Error type",
"message": "Human-readable error message"
}
Common HTTP Status Codes:
200- Success400- Bad Request (invalid data)401- Unauthorized (invalid API key)404- Not Found500- Server Error
Example Error Handling:
try {
const response = await fetch(`${BTAB_API_URL}/my-products`, {
headers: { 'Authorization': `Bearer ${BTAB_API_KEY}` }
});
if (!response.ok) {
const error = await response.json();
console.error('API Error:', error.message);
if (response.status === 401) {
// Invalid API key
console.error('Check your API key in .env file');
}
}
const data = await response.json();
// Use data...
} catch (error) {
console.error('Network error:', error);
}
Technology-Specific Examples
Next.js (App Router)
API Route: app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
const BTAB_API_KEY = process.env.BTAB_API_KEY!;
const BTAB_API_URL = process.env.BTAB_API_URL!;
export async function GET(request: NextRequest) {
try {
const response = await fetch(`${BTAB_API_URL}/my-products`, {
headers: {
'Authorization': `Bearer ${BTAB_API_KEY}`
}
});
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
);
}
}
PHP (WordPress Plugin)
<?php
// wp-content/plugins/btab-integration/btab-api.php
class BTAB_API {
private $api_key;
private $api_url = 'https://api.btab.app/api/v1';
public function __construct() {
$this->api_key = get_option('btab_api_key');
}
public function get_products() {
$response = wp_remote_get($this->api_url . '/my-products', [
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key
]
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true);
}
public function create_order($order_data) {
$response = wp_remote_post($this->api_url . '/orders', [
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json'
],
'body' => json_encode($order_data)
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true);
}
}
Python (Flask)
# app.py
import os
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
BTAB_API_KEY = os.getenv('BTAB_API_KEY')
BTAB_API_URL = os.getenv('BTAB_API_URL', 'https://api.btab.app/api/v1')
@app.route('/api/products')
def get_products():
try:
response = requests.get(
f'{BTAB_API_URL}/my-products',
headers={'Authorization': f'Bearer {BTAB_API_KEY}'}
)
response.raise_for_status()
return jsonify(response.json())
except requests.exceptions.RequestException as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/checkout', methods=['POST'])
def checkout():
try:
order_data = request.json
response = requests.post(
f'{BTAB_API_URL}/orders',
json=order_data,
headers={
'Authorization': f'Bearer {BTAB_API_KEY}',
'Content-Type': 'application/json'
}
)
response.raise_for_status()
return jsonify(response.json())
except requests.exceptions.RequestException as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(port=8000)
Best Practices
Security
-
Never expose API key in:
- Browser JavaScript
- Mobile app code
- GitHub repository
- Client-side config files
-
Always store API key in:
- Environment variables (
.envfile) - Server-side config
- Secure secret management (AWS Secrets, Vault, etc.)
- Environment variables (
-
Use HTTPS everywhere - Both your site and BTAB API use HTTPS
-
Validate data before sending to BTAB API:
// Validate customer email if (!isValidEmail(customer.email)) { return res.status(400).json({ error: 'Invalid email' }); } // Validate pricing matches const product = await getProduct(item.product_id); if (item.price_cents !== product.retail_price_cents) { return res.status(400).json({ error: 'Price mismatch' }); }
Performance
-
Cache product data - Don't fetch on every page load:
// Refresh products every 5 minutes let productsCache = null; let lastFetch = 0; async function getProducts() { const now = Date.now(); if (productsCache && (now - lastFetch) < 5 * 60 * 1000) { return productsCache; } productsCache = await fetchFromBTAB(); lastFetch = now; return productsCache; } -
Handle rate limits - Implement retry logic:
async function fetchWithRetry(url, options, retries = 3) { for (let i = 0; i < retries; i++) { const response = await fetch(url, options); if (response.status === 429) { // Rate limited, wait and retry await sleep(1000 * (i + 1)); continue; } return response; } throw new Error('Max retries exceeded'); }
Testing
Test in Development: Use the test API key from dashboard for local testing.
Example Test:
// test-api.js
require('dotenv').config();
async function testAPI() {
console.log('Testing BTAB API integration...\n');
// Test 1: Get products
console.log('1. Fetching products...');
const response = await fetch(`${process.env.BTAB_API_URL}/my-products`, {
headers: { 'Authorization': `Bearer ${process.env.BTAB_API_KEY}` }
});
if (!response.ok) {
console.error('❌ Failed:', await response.text());
return;
}
const data = await response.json();
console.log(`✅ Success: Found ${data.products.length} products\n`);
// Test 2: Create test order
console.log('2. Creating test order...');
// ... test order creation
}
testAPI();
Troubleshooting
"401 Unauthorized"
- Check API key is correct in
.envfile - Verify key starts with
btab_live_orbtab_test_ - Make sure Authorization header is properly formatted
"No products returned"
- Make sure you've added products in the dashboard
- Check you're calling
/my-productsnot/products
"CORS errors"
- You should NOT get CORS errors if using backend proxy correctly
- If you see CORS errors, you're calling BTAB API from browser (wrong!)
"Rate limit exceeded"
- Implement caching (see Performance section)
- Add retry logic with backoff
Support
Dashboard: https://dashboard.btab.app API Status: https://api.btab.app/health Documentation: https://btab.app/developers
Example code
A working webhook consumer — signature check, retries, idempotent handling — ships with these docs: examples/webhook-consumer.js. It is the one example BTAB publishes.
Happy Selling! 🚀