API reference
A small JSON REST API for creating and emailing invoices. Base URL: https://cbdcoffee.store
Authentication
Create an API key in your dashboard (API keys tab). Send it on every request. Keys start with inv_live_ and are shown only once, so store them in an environment variable or secrets manager and never in front-end code.
Authorization: Bearer inv_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
The invoice object
| Field | Type | Notes |
|---|---|---|
| id | string | Assigned by the server, e.g. INV-1001 |
| client | string | Required. Client or company name (max 200 chars) |
| string | Required. Where the invoice is sent | |
| items | array | Required. 1 to 100 items: { desc, qty, price } |
| tax | number | Tax percentage applied to the subtotal (0 to 100) |
| currency | string | USD, EUR, GBP or INR (default USD) |
| due | string | Due date, YYYY-MM-DD |
| notes | string | Free text printed on the invoice (max 1000 chars) |
| status | string | draft, sent or paid |
| created / sentAt | string | ISO 8601 timestamps |
| subtotal, taxAmount, total | number | Calculated by the server, read-only |
{
"id": "INV-1001", "client": "Acme Ltd", "email": "billing@acme.com",
"items": [{ "desc": "Web design", "qty": 1, "price": 1200 }],
"tax": 10, "currency": "USD", "due": "2026-12-01", "notes": "",
"status": "draft", "created": "2026-11-09T10:00:00.000Z",
"subtotal": 1200, "taxAmount": 120, "total": 1320
}
Endpoints
GET /v1/invoices
List your invoices, newest first. Optional ?status=draft|sent|paid.
POST /v1/invoices
Create a draft invoice. Returns 201 and the invoice. Requires an active trial or plan.
GET /v1/invoices/:id
Fetch one invoice.
PATCH /v1/invoices/:id
Update any field, or set status to paid. Send only the fields you want to change.
DELETE /v1/invoices/:id
Delete an invoice. Returns 204 with no body.
POST /v1/invoices/:id/send
Email the invoice and PDF to the client and mark it as sent. Counts as one invoice against your quota. Requires a verified email address.
{ "message": "Invoice emailed to billing@acme.com",
"quota": { "period": "day", "limit": 100, "used": 7 },
"invoice": { "id": "INV-1001", "status": "sent", "...": "..." } }
GET /v1/stats
Totals for outstanding and paid invoices, plus your current quota.
Bulk sending
Send the CSV text as a JSON string. Maximum 5,000 rows and 5 MB per upload. Columns: client, email, description, qty, price (required) and tax, currency, due, notes, group. Download the template.
POST /v1/bulk/preview
Body { "csv": "..." }. Validates without creating anything. Returns row and invoice counts, up to 50 errors with line numbers, a sample, and your remaining quota.
POST /v1/bulk
Body { "csv": "...", "send": true, "skipInvalid": false }. Creates the invoices and queues them for sending (send: false creates drafts only). Returns 202 and a batchId. Fails with 422 if rows have errors and skipInvalid is not true, 429 if the file exceeds your remaining quota, and 409 if a batch is already sending.
GET /v1/bulk and GET /v1/bulk/:id
Batch progress: total, sent, failed, pending, status. The single-batch endpoint also lists failures with the reason.
E-signature
POST /v1/docsign/upload?title=Contract
Upload a PDF as the raw request body (Content-Type: application/pdf, up to 10 MB and 100 pages; password-protected PDFs are refused). Returns 201 with the draft id, pages and page sizes.
POST /v1/docsign/:id/send
Place fields and send. Body: signers (array of {name, email}, 1 to 10), fields (array of {signer, type, page, x, y, w, h, required} where signer is the index in signers, type is signature, date, name or text, page starts at 0 and x, y, w, h are fractions from 0 to 1 of the page, measured from the top-left), optional order (parallel or sequential), message, title and expires_days. Every signer needs at least one signature field. Each signer counts as one send.
GET /v1/docsign, GET /v1/docsign/:id, GET /v1/docsign/:id/signed, POST /v1/docsign/:id/cancel, DELETE /v1/docsign/:id
List, inspect, download the completed PDF (with audit page), cancel a pending request, or delete a document.
POST /v1/esign
Emails a signature request. Body: title, body (the document text, 10 to 20,000 characters), signer_name, signer_email, optional message and expires_days (7, 14 or 30). Counts as one send. Returns 201.
GET /v1/esign, GET /v1/esign/:id
List requests or fetch one. Status is pending, signed, declined, cancelled or expired.
POST /v1/esign/:id/cancel
Cancel a pending request.
GET /v1/esign/:id/pdf
Download the PDF with the signature and audit trail (once signed).
Rate limits and quotas
Requests per minute depend on your plan (Free trial: 60, up to 1,000 on Business). Each response includes X-RateLimit-Limit and X-RateLimit-Remaining. When exceeded you get 429 and a Retry-After header in seconds.
Sending quota is counted per email sent: each invoice (single or bulk) and each e-signature request. A successful send returns X-Quota-Limit and X-Quota-Remaining. Daily quotas reset at 00:00 UTC.
| Plan | Price | Sends (invoices + e-sign requests) | API keys | API requests / min | AI messages / day |
|---|---|---|---|---|---|
| Free trial (7 days) | $0 | 100 per day | 1 | 60 | 30 |
| Starter | $10 / month | 500 per day | 3 | 60 | 100 |
| Growth | $25 / month | 2,000 per day | 10 | 300 | 300 |
| Business | $50 / month | 10,000 per day | 50 | 1,000 | 1,000 |
Errors
| Status | Meaning | What to do |
|---|---|---|
| 400 | Body is not valid JSON | Fix the request body |
| 401 | Missing or invalid API key | Check the Authorization header; the key may be revoked |
| 402 | Trial or plan has ended | Subscribe to continue creating and sending. Reading and deleting still work |
| 403 | Email not verified, or key limit reached | Verify your email, or revoke an unused key |
| 404 | Invoice not found | Check the id |
| 422 | Validation failed | The error field explains what to correct |
| 429 | Rate limit or invoice quota reached | Wait for Retry-After, or upgrade for a higher limit |
| 502 | Email delivery failed | Retry later. Your quota is not charged for failed sends |
{ "error": "Invoice limit reached (100 per day).", "quota": { "period": "day", "limit": 100, "used": 100 } }
Code samples
cURL
curl -X POST https://cbdcoffee.store/v1/invoices \
-H "Authorization: Bearer $INVOICE_API_KEY" -H "Content-Type: application/json" \
-d '{"client":"Acme Ltd","email":"billing@acme.com","tax":10,"items":[{"desc":"Web design","qty":1,"price":1200}]}'
curl -X POST https://cbdcoffee.store/v1/invoices/INV-1001/send -H "Authorization: Bearer $INVOICE_API_KEY"
JavaScript (Node 18+)
const base = "https://cbdcoffee.store";
const headers = { Authorization: "Bearer " + process.env.INVOICE_API_KEY, "Content-Type": "application/json" };
const res = await fetch(base + "/v1/invoices", { method: "POST", headers,
body: JSON.stringify({ client: "Acme Ltd", email: "billing@acme.com", items: [{ desc: "Web design", qty: 1, price: 1200 }] }) });
const invoice = await res.json();
const sent = await fetch(base + "/v1/invoices/" + invoice.id + "/send", { method: "POST", headers });
if (sent.status === 429) console.log("Limit reached, retry after", sent.headers.get("Retry-After"));
Python
import os, requests
base = "https://cbdcoffee.store"
h = {"Authorization": "Bearer " + os.environ["INVOICE_API_KEY"]}
inv = requests.post(base + "/v1/invoices", headers=h, json={
"client": "Acme Ltd", "email": "billing@acme.com",
"items": [{"desc": "Web design", "qty": 1, "price": 1200}]}).json()
requests.post(base + "/v1/invoices/" + inv["id"] + "/send", headers=h)
id on your side.