An OpenCart extension that syncs products, stock, prices, orders, and images between an OpenCart store and a Reverb.com marketplace listing.
Type: OpenCart 3.x Extension (with planned OpenCart 4.x compatibility)
Primary Market: Australia (reverb.com/au)
Sync Direction: Bidirectional (OpenCart ↔ Reverb)
https://api.reverb.com/api/Authorization: Bearer <token> + Accept-Version: 3.0 headersThe extension must provide an admin panel (Admin > Extensions > Modules > Reverb) that allows the store owner to:
Each product in the OpenCart admin must show a toggle/checkbox to:
| Field | OC → Reverb | Reverb → OC |
|---|---|---|
| Title / Name | ✓ | ✓ |
| Description | ✓ | ✓ |
| Price | ✓ | ✓ |
| Stock / Qty | ✓ | ✓ |
| Images | ✓ | — |
| Orders | — | ✓ |
upload/
├── admin/
│ ├── controller/extension/module/
│ │ └── reverb.php # Admin settings, manual sync, order import, AJAX endpoints
│ ├── language/en-gb/extension/module/
│ │ └── reverb.php # All user-facing strings
│ ├── model/extension/module/
│ │ └── reverb.php # DB interactions, sync logic, order import
│ └── view/template/extension/module/
│ ├── reverb.twig # Admin settings form (tabs: Settings, Category Mapping, Sync Log)
│ └── reverb_product.twig # Per-product Reverb tab (loaded via AJAX)
├── catalog/
│ └── controller/extension/module/
│ └── reverb.php # Webhook receiver + cron polling endpoint
└── system/
└── library/reverb/
├── ReverbApi.php # HTTP client wrapping the Reverb REST API
├── ProductMapper.php # Maps OC product fields ↔ Reverb listing fields
└── OrderMapper.php # Maps Reverb order payload → OC order format
OCMOD patch file (install.xml) injects the per-product Reverb tab into the product edit page without modifying core files.
The upload/ folder and install.xml are zipped into reverb.ocmod.zip for installation via Extensions > Installer in the OpenCart admin backend.
The extension creates and manages the following tables.
oc_reverb_product_map
| Column | Type | Notes |
|---|---|---|
product_id |
INT | FK to oc_product |
reverb_listing_id |
VARCHAR(64) | Reverb's listing ID once synced |
sync_enabled |
TINYINT(1) | Per-product on/off toggle (default 0) |
condition_uuid |
VARCHAR(64) | Reverb condition UUID |
reverb_category_uuid |
VARCHAR(64) | Reverb category UUID (overrides mapping) |
handmade |
TINYINT(1) | Whether the item is handmade (default 0) |
upc_does_not_apply |
TINYINT(1) | Whether item is UPC/EAN exempt (default 1) |
last_synced_at |
DATETIME | Timestamp of last successful sync |
oc_reverb_order_map
| Column | Type | Notes |
|---|---|---|
reverb_order_number |
VARCHAR(64) | Primary key; Reverb order number |
order_id |
INT | Corresponding OC order ID |
reverb_status |
VARCHAR(32) | Reverb order status at time of import |
created_at |
DATETIME | When the order was imported |
oc_reverb_sync_log
| Column | Type | Notes |
|---|---|---|
log_id |
INT AUTO_INCREMENT | |
product_id |
INT | |
direction |
ENUM('push','pull') | |
status |
ENUM('success','error') | |
message |
TEXT | Error detail or summary |
created_at |
DATETIME |
API token, sync mode, and category whitelist are stored in OpenCart's native oc_setting table via $this->config — no custom config table needed.
oc_setting) with the key prefix module_reverb_. Never stored in a raw custom table.oc_reverb_product_map.sync_enabled; defaults to 0 (disabled) for all products.sync_enabled = 1 are synced.catalog/controller/extension/module/reverb/cron) for stock/price polling.PUT /listings/{id} payload ({"photos": ["https://..."], "publish": true}). There is no separate photo upload endpoint in Reverb API v3. Images are pushed OC → Reverb only; never pulled back. The store URL is read from config_url with fallback to HTTPS_CATALOG / HTTP_CATALOG constants from admin/config.php.oc_reverb_order_map keyed by reverb_order_number. Both the admin import action and the catalog webhook handler check this table before creating a new OC order./categories/flat and cached in oc_setting for 24 hours. The full_name field (e.g. "Guitars > Electric Guitars") is parsed to group categories by root name for <optgroup> rendering in dropdowns.ProductMapper::toReverb() applies html_entity_decode() to the title and converts HTML markup to plain text for the description before sending to Reverb.Base URL: https://api.reverb.com/api/
Required headers on every request:
Authorization: Bearer <token>
Accept-Version: 3.0
Content-Type: application/hal+json
Rate limit: ~100 requests/min on the standard plan — batch operations where possible.
Key endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/my/listings |
GET | List all existing seller listings |
/listings |
POST | Create a new listing |
/listings/{id} |
PUT | Update listing (also accepts photos URL array and publish: true) |
/listings/{id}/end |
PUT | End/delist a listing |
/my/orders/selling/all |
GET | Fetch all seller orders (supports updated_start_date, page, per_page) |
/my/orders/selling/{order_number} |
GET | Fetch a single order |
/webhooks |
POST | Register a webhook endpoint |
/categories/flat |
GET | Get the full Reverb category tree (each entry has uuid, full_name, name) |
/listing_conditions |
GET | Get available item conditions |
When creating API token in Reverb these are the available scopes:
| Scope | Permission |
|---|---|
| public | Read publicly available data |
| read_listings | Read all of your listings with sales and bump data |
| write_listings | Create/update listings (inventory, price, etc) |
| read_orders | Read all your orders |
| write_orders | Update the status of your orders |
| read_profile | Get account and shop details |
| write_profile | Update shop settings |
| read_feedback | Read feedback sent or received |
| write_feedback | Write feedback about transactions |
| read_messages | Retrieve messages |
| write_messages | Post and update messages |
| read_offers | Read offers |
| write_offers | Make offers on listings |
| read_reviews | Read listing reviews |
| write_reviews | Write listing reviews |
| read_lists | Read watch list / feed |
| write_lists | Update watch list / feed |
| read_payouts | Read payout data |
| read_addresses / write_addresses | Read / write addresses |
sync_enabled = 1 AND is in an allowed category.ProductMapper::toReverb():
html_entity_decode() appliedstrip_tags(), html_entity_decode()make (manufacturer), handmade, upc_does_not_apply, condition, category, shipping ratesphotos array and publish: true to the payload.reverb_listing_id exists → PUT /listings/{id}; otherwise → POST /listings and store the returned ID.last_synced_at and log result in oc_reverb_sync_log.extension/module/reverb/importOrders (AJAX).importOrdersFromReverb(): paginates GET /my/orders/selling/all with optional updated_start_date from last import.oc_reverb_order_map) and non-actionable statuses (cancelled, unpaid, etc.).oc_order, oc_order_product, oc_order_total, oc_order_history. Decrements OC product stock.oc_reverb_order_map. Updates module_reverb_order_last_sync setting.catalog/controller/extension/module/reverb/webhook (or cron polls listings + orders).listing/update: look up OC product by reverb_listing_id, update price/stock/description.order/create: check oc_reverb_order_map for duplicates, map via OrderMapper::toOpenCart(), create via model_checkout_order->addOrder(), save to oc_reverb_order_map.namespace Opencart\Admin\Controller\Extension\Module;\Opencart\System\Engine\Controllersystem/library/reverb/ classes between both versions (they are framework-agnostic)$this->db->query() and $this->db->escape() for all database access — never raw PDO or unsanitised inputoc_reverb_sync_log rather than surfacing raw exceptions to the UImigrate() called from saveProductMap() and getProductMap() — uses information_schema to add columns only if missing, safe to run repeatedly