Pixeldev

Work/Vinoshipper for WooCommerce

Selling wine into every US state from a normal WooCommerce store, without a licence in each one.

US wineries can only ship direct to consumers in states where they hold a licence. Vinoshipper covers the rest, but it wants to own the checkout. I built a WooCommerce plugin that keeps the winery's own store and payment gateway, and quietly routes the right orders to Vinoshipper after payment.

Product
Vinoshipper for WooCommerce (WordPress plugin)
Industry
Wine, direct-to-consumer shipping
Services
Plugin development, API integration
Stack
PHP 8, WooCommerce, React, Tailwind, shadcn/ui
Timeline
December 2025 to January 2026
The Orders tab of the plugin inside wp-admin: a table of WooCommerce orders with their Vinoshipper IDs, status badges such as Shipped, Failed and Compliance Rejected, tracking numbers and Retry buttons.
Every order sent to Vinoshipper, with its status, tracking and a one-click retry when something needs fixing.
51states and DC that can be switched to Vinoshipper individually
9REST endpoints behind the React admin and the webhook
3wineries running it

The problem

In the US, a winery shipping straight to a customer needs a licence for the state the wine is going to. Most small wineries hold a handful. Vinoshipper holds the licences and handles the compliance for the rest, but its standard setup assumes you sell through its checkout.

A winery that already runs WooCommerce, with its own payment gateway, wine club and design, doesn't want two checkouts. It wants one store, where an order to a licensed state ships as normal and an order anywhere else goes to Vinoshipper without anyone retyping it. I first built it for a family winery in Sonoma that was turning away orders from states it wasn't licensed for, and now offer it to other wineries in the same spot.

What I built

A WooCommerce plugin with a React admin inside wp-admin. Payment happens through whatever gateway the store already uses (Square, Stripe or anything else). Once it clears, the plugin checks the shipping state against the winery's list and, if it matches, submits the order to Vinoshipper's API as already paid, matched to Vinoshipper's catalogue by SKU.

Vinoshipper then runs its compliance checks and ships. Its webhooks come back to the store: tracking numbers land on the order and in the customer's account, delivered orders are marked complete, and a compliance rejection puts the order on hold and emails the winery.

  1. 1State routing: pick any of the 50 states plus DC, grouped by region, and only those orders go to Vinoshipper.
  2. 2A Vinoshipper shipping method for checkout that shows live carrier rates and delivery estimates, cached for 15 minutes per address and basket.
  3. 3Carrier mapping for the store's other methods (flat rate, free shipping, local pickup) across UPS, FedEx, GLS and self-delivery, with 12 service levels.
  4. 4An orders table and an order meta box showing sync status, the Vinoshipper ID, tracking and errors, with retry built into WooCommerce's own order actions.
  5. 5A webhook log that keeps every incoming event and its raw payload, so a missing tracking number can be traced in minutes.
The API tab of the plugin, showing an explanation of how shipping methods work, default carrier and rate code selects, and a table mapping WooCommerce shipping methods such as Flat rate and Local pickup to Vinoshipper carriers and rate codes.
Carrier mapping: the store's existing shipping methods each get a Vinoshipper carrier and service level, so nothing changes for the customer at checkout.

The hard part

Two systems each think they own the order, so the work is in keeping them from disagreeing. WooCommerce can fire both its payment complete and processing hooks for the same order, so submission is guarded twice: a unique key on the WooCommerce order ID in the sync table, and a status check that skips anything already submitted, processing, shipped or delivered. An order goes to Vinoshipper once, however many hooks fire.

Vinoshipper's webhooks aren't signed, and the event names and payload shapes aren't consistent. The handler accepts eleven event names across six handlers, reads tracking from three different payload shapes, and only ever acts on an order already in its own sync table, so a stray or replayed request can't touch an order it didn't send. Everything is logged with the source IP.

Outbound calls retry with exponential backoff on rate limits, server errors and dropped connections, but not on validation errors, which go straight to the winery as a readable message (usually a product without a SKU). The API secret is stored encrypted with AES-256 using the site's own salts, not as plain text in the options table.

The Logs tab of the plugin with an expanded order.shipped webhook event showing the source IP, a Tracking updated successfully message and the JSON payload with UPS tracking details.
The webhook log: every event from Vinoshipper, what the plugin did with it, and the raw payload.

How it went

The customer sees one store. They pick a shipping rate, pay with the store's usual gateway, and get tracking in their account like any other order. The winery sees Vinoshipper orders alongside the rest in WooCommerce, with problems flagged rather than discovered. For that first winery, it opened up 39 more states without a second checkout, routed more than 400 orders to Vinoshipper in its first three months, and saved its team around five hours a week of retyping orders.

It's built on the WooCommerce plugin template I use for my own products, with PSR-4 classes, a small dependency container and HPOS-compatible order screens, so it's ready for a paid licence and more wineries.

A checkout page on a fictional winery website, Hollow Creek Cellars, with a North Carolina shipping address and three Wine Shipping options showing UPS rates and estimated delivery dates.
At checkout, live Vinoshipper rates sit in the store's own design (shown on a fictional winery).