Saleor integration

Tariffs API for Saleor.

Wire US duty calculation into a Saleor checkout using a Saleor App that listens to checkout webhooks and calls Tariffs API.

Saleor's App framework is the right place to bolt on tax-style data. A small app subscribes to CHECKOUT_CALCULATE_TAXES or a calculate_taxes synchronous webhook, calls the Tariffs API resolve endpoint, and returns the duty amount back to Saleor as an extra line item or shipping price. This page shows the shape of that app.

Why this works

Built for Saleor developers.

Webhook-native

Saleor's sync webhook for tax calculation is the same hook tax providers use. We slot into it cleanly.

TypeScript or Python

Build the app with Saleor's official app-sdk in TypeScript or a small FastAPI service in Python. Either works.

No GraphQL gymnastics

Your app handles the GraphQL side toward Saleor. Toward us, it's a single REST call. No client library to learn.

US-stacking correct

Returns base duty plus Section 301, 232, IEEPA, and Chapter 99 in one call. You forward the total to Saleor.

In your code

Drop it in.

Skeleton of a Saleor App webhook handler. The full app needs install + permission setup per the Saleor docs.

1. Saleor App webhook (TypeScript, app-sdk)

typescript
import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"
import { saleorApp } from "../../../saleor-app"

export const calculateTaxesWebhook = new SaleorSyncWebhook({
  name: "Calculate taxes (duty)",
  webhookPath: "api/webhooks/calculate-taxes",
  event: "CHECKOUT_CALCULATE_TAXES",
  apl: saleorApp.apl,
  query: "...",
})

export default calculateTaxesWebhook.createHandler(async (req, res, ctx) => {
  const lines = ctx.payload.taxBase.lines
  let dutyTotal = 0
  for (const line of lines) {
    const hts = line.sourceLine.variant?.product?.metadata?.find(m => m.key === "hts_code")?.value
    const origin = line.sourceLine.variant?.product?.metadata?.find(m => m.key === "country_of_origin")?.value
    if (!hts || !origin) continue

    const r = await fetch(
      `https://tariffsapi.com/api/v1/tariffs/resolve?hts=${hts}&origin=${origin}`,
      { headers: { Authorization: `Bearer ${process.env.TARIFFSAPI_KEY}` } }
    )
    const data = await r.json()
    const rate = data.summary.total_resolved_ad_valorem_rate / 100
    dutyTotal += Number(line.unitAmount) * line.quantity * rate
  }

  return res.status(200).json({
    shipping_price_gross_amount: ctx.payload.taxBase.shippingPrice.amount,
    shipping_price_net_amount: ctx.payload.taxBase.shippingPrice.amount,
    shipping_tax_rate: 0,
    lines: lines.map(l => ({
      tax_rate: 0,
      total_gross_amount: Number(l.unitAmount) * l.quantity,
      total_net_amount: Number(l.unitAmount) * l.quantity,
    })),
    duty_total: dutyTotal,
  })
})

2. Python alternative (FastAPI)

python
import os
import httpx
from fastapi import FastAPI, Request

app = FastAPI()
KEY = os.environ["TARIFFSAPI_KEY"]

@app.post("/api/webhooks/calculate-taxes")
async def calculate_taxes(req: Request):
    payload = await req.json()
    duty_total = 0.0
    async with httpx.AsyncClient() as client:
        for line in payload["taxBase"]["lines"]:
            meta = {m["key"]: m["value"] for m in (line["sourceLine"]["variant"]["product"]["metadata"] or [])}
            hts, origin = meta.get("hts_code"), meta.get("country_of_origin")
            if not (hts and origin):
                continue
            r = await client.get(
                f"https://tariffsapi.com/api/v1/tariffs/resolve",
                params={"hts": hts, "origin": origin},
                headers={"Authorization": f"Bearer {KEY}"},
            )
            data = r.json()
            rate = data["summary"]["total_resolved_ad_valorem_rate"] / 100
            duty_total += float(line["unitAmount"]) * line["quantity"] * rate
    return {"duty_total": duty_total}
What you get back

One endpoint. Deterministic JSON.

The resolve endpoint returns a single JSON object per HTS code and origin pair. For a multi-line cart, the bulk resolve_batch endpoint accepts up to 200 codes in one call, which keeps webhook latency low.

GET /api/v1/tariffs/resolve?hts=8541.10.00.80&origin=CN

{
  "summary": {
    "applicable_ad_valorem_rate": 0.0,
    "resolved_additional_ad_valorem_rate": 25.0,
    "total_resolved_ad_valorem_rate": 25.0
  },
  "base_tariff": { "percentage_component": 0.0 },
  "additional_measures": [
    {
      "program": "section_301",
      "chapter_99_code": "9903.91.05",
      "resolved_rate": { "percentage_component": 0.25 }
    }
  ]
}
FAQ

Saleor + Tariffs API

Where do I store the HTS code on a Saleor product?

Product metadata. Saleor's metadata API lets you set arbitrary key-value pairs per product or variant. Store hts_code and country_of_origin as keys and read them in the webhook handler.

Is this the same as Saleor's tax integration?

Yes, it uses the same CHECKOUT_CALCULATE_TAXES sync webhook that tax providers (Avalara, Taxjar) use. The only difference is what the app returns: instead of sales tax, it returns the duty amount.

Can I run the app on the same server as Saleor?

Yes. A Saleor App is just an HTTP service that responds to webhooks. Deploy it anywhere Saleor can reach via HTTPS.

Latency concerns?

Tariffs API's resolve endpoint is fast (sub-200ms p95). For multi-line checkouts, batch via resolve_batch to keep total webhook time under a second.

Sources

Add US duty to your Saleor checkout in an afternoon.