Docs / API reference

REST API v1

JSON in, signed PDF URLs out. Designed for Shopify apps, freight-forwarder dashboards, and custom internal tooling.

Base URL

https://customs-invoice.com/api/v1

All endpoints are HTTPS-only. Requests over HTTP redirect to HTTPS.

CBAM compliance API

Scope check, single-shipment cost calculator, and bulk processor for ERP / TMS integrations are documented separately:

Read the CBAM REST API docs →

Authentication

Send your key in the Authorization header as a bearer token:

Authorization: Bearer cik_a3d89ed455260775734e52cd4efd3ace7965a6a0bc12d4f8

Keys are 48 hex characters with a cik_ prefix. Mint / revoke at /settings/api-keys. Because we store SHA-256 hashes only, the raw value appears once at creation — copy it into your secret manager immediately.

Create an invoice

POST/api/v1/invoices

Request headers

Authorization: Bearer <your-key>
Content-Type: application/json

Request body

FieldTypeRequiredNotes
invoiceDataInvoiceDatayesSee Data types below.
documentsstring[]noOverride the documents generated. If omitted we render invoiceData.primaryDocument plus its additionalDocuments.
emailstringnoDelivery email. Defaults to the email on your account.

Response — 200 OK

{
  "id": "e9d1a3b0-...-...",
  "shortId": "INV-2026-ABC123",
  "documents": [
    {
      "type": "commercial",
      "signedUrl": "https://.../invoices/e9d1.../commercial.pdf?token=...",
      "expiresIn": 3600
    }
  ]
}

Signed URLs are valid for 1 hour. To refresh, fetch /invoice/<id> in the dashboard or POST again.

Data types

InvoiceData

{
  invoiceNumber:        string      // your reference, e.g. "INV-2026-001"
  invoiceDate:          string      // YYYY-MM-DD
  currency:             Currency    // "USD" | "EUR" | "GBP" | ...
  incoterms:            Incoterm    // "FOB" | "CIF" | "DDP" | ... (Incoterms 2020)
  transportMode:        string      // "air" | "sea" | "road" | "courier"
  portOfLoading:        string?
  portOfDischarge:      string?
  shipper:              Party
  consignee:            Party
  lineItems:            LineItem[]  // 1..500
  freight:              number?
  insurance:            number?
  otherCharges:         number?
  totalDeclaredValue:   number      // you compute this; must match items + charges
  declarationText:      string?     // commercial/proforma footer
  primaryDocument:      "commercial" | "proforma"
  additionalDocuments:  ("packing-list" | "certificate-of-origin")[]
  validUntil:           string?     // proforma only, YYYY-MM-DD
  proformaPurpose:      string?
  manufacturerSameAsShipper: boolean?  // CoO only
  manufacturer:         Party?      // CoO only; when not same as shipper
  originStatement:      string?
}

Party

{
  name:    string
  address: string
  city:    string
  state:   string?
  zip:     string
  country: string   // ISO 3166-1 alpha-2
  phone:   string?
  email:   string?
  taxId:   string?  // VAT / EIN / EORI etc.
}

LineItem

{
  id:              string      // client-assigned id (we don't validate)
  description:     string
  hsCode:          string      // e.g. "7318.15"
  countryOfOrigin: string      // ISO alpha-2
  quantity:        number
  unit:            string      // PCS, KG, SET, MTR, LTR, BOX, PAL, PKG
  unitPrice:       number
  totalValue:      number      // quantity * unitPrice

  // Packing-list only (ignored otherwise)
  packages:        number?
  netWeightKg:     number?
  grossWeightKg:   number?
  dimensionsCm:    string?     // "L × W × H"
  marksAndNumbers: string?
}

Code samples

curl

curl -X POST https://customs-invoice.com/api/v1/invoices \
  -H "Authorization: Bearer $CUSTOMS_INVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @shipment.json

Node.js (native fetch, ≥ 18)

async function createInvoice(invoiceData) {
  const res = await fetch(
    "https://customs-invoice.com/api/v1/invoices",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.CUSTOMS_INVOICE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        invoiceData,
        documents: ["commercial", "packing-list"],
      }),
    },
  );
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${res.status} ${error}`);
  }
  return res.json();
}

Python (requests)

import os, requests

def create_invoice(invoice_data: dict):
    r = requests.post(
        "https://customs-invoice.com/api/v1/invoices",
        headers={
            "Authorization": f"Bearer {os.environ['CUSTOMS_INVOICE_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={
            "invoiceData": invoice_data,
            "documents": ["commercial", "packing-list"],
        },
        timeout=120,
    )
    r.raise_for_status()
    return r.json()

Go (net/http)

req, _ := http.NewRequest("POST", "https://customs-invoice.com/api/v1/invoices",
    bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("CUSTOMS_INVOICE_API_KEY"))
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
defer resp.Body.Close()

Limits

  • Rate limit: 60 invoices per hour per key. Returns 429 on overage. Higher throughput available on request.
  • Payload size: 1 MB, up to ~500 line items per invoice.
  • Document retention: 30 days. Download and persist on your side.
  • Signed URL validity: 1 hour. Re-fetch after expiry.
  • Request timeout: 120 seconds (enough for ~4 PDFs in one call).

Webhooks

Not yet available on v1. Planned events: invoice.ready (all PDFs uploaded), invoice.expired (30-day retention hit), batch.completed (bulk upload finished).

Let us know what you need first at hello@customs-invoice.com.

Ready to ship?

Mint a key and send your first invoice in under five minutes.

Create a key