@maxime-ns/back-market
v0.5.0
Published
A back market component for Convex.
Downloads
25
Readme
@maxime-ns/back-market
A Convex component for integrating Back Market's Marketplace Orders API into your Convex application.
Features
- Connection Management - Connect, disconnect, and manage Back Market API credentials per organization
- Order Syncing - Automatic order sync every 5 minutes via cron job
- Orderlines - Query and update individual orderlines with IMEI support
- Listings Management - Sync, update, and bulk create/update listings via CSV
- BackShip Integration - Full support for deliveries and returns with label storage
- Real-time Data - Query synced data from your Convex database in real-time
- Multi-tenant - Support multiple organizations with separate API keys
- Environment Support - Switch between sandbox and production environments
Quick Start
1. Install the Component
npm install @maxime-ns/back-market2. Add to Your Convex App
Create or update convex/convex.config.ts:
import { defineApp } from "convex/server";
import backMarket from "@maxime-ns/back-market/convex.config.js";
const app = defineApp();
app.use(backMarket);
export default app;3. Get Your Back Market API Key
- Go to Back Market Developer Portal
- Create an application or use an existing one
- Copy your API key (for sandbox testing, use the sandbox API key)
4. Understand Multi-Tenancy
This component is designed to support multiple organizations, each with
their own Back Market API key. The organizationId parameter is a string that
you provide from your host application.
Single Organization (Simple Setup)
If your app only needs one Back Market connection, you can use any constant string as the organization ID:
// Use a constant for single-tenant apps
const ORGANIZATION_ID = "default";
// Or use the authenticated user's ID
const organizationId = identity.subject;Multiple Organizations (Multi-Tenant Setup)
For multi-tenant applications where each organization has their own Back Market account, pass the organization ID from your auth system. The component will:
- Store separate API keys per organization
- Sync orders independently for each organization
- Keep data isolated between organizations
5. Use the Component
Create convex/backmarket.ts:
import { action, mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { BackMarket } from "@maxime-ns/back-market";
import { v } from "convex/values";
const backMarket = new BackMarket(components.backMarket);
// Connect an organization to Back Market
export const connect = mutation({
args: {
organizationId: v.string(),
apiKey: v.string(),
},
handler: async (ctx, args) => {
return await backMarket.upsertConnection(ctx, {
organizationId: args.organizationId,
apiKey: args.apiKey,
});
},
});
// Get connection status
export const getConnectionStatus = query({
args: { organizationId: v.string() },
handler: async (ctx, args) => {
return await backMarket.getConnectionStatus(ctx, {
organizationId: args.organizationId,
});
},
});
// Get orders from local database
export const getOrders = query({
args: {
organizationId: v.string(),
state: v.optional(v.number()),
},
handler: async (ctx, args) => {
return await backMarket.getOrders(ctx, {
organizationId: args.organizationId,
state: args.state,
});
},
});
// Manually sync orders from Back Market API
export const syncOrders = action({
args: { organizationId: v.string() },
handler: async (ctx, args) => {
return await backMarket.syncOrders(ctx, {
organizationId: args.organizationId,
environment: "production",
});
},
});Order States
Back Market uses numeric states for orders:
| State | Description |
| ----- | ---------------------------------------------- |
| 0 | New Order - Payment validation pending |
| 1 | Paid - Payment validated, process orderlines |
| 3 | Shipping pending - Ready to ship |
| 8 | Not paid - Payment failed |
| 9 | Processed - Package shipped |
| 10 | Pending - Customer ordered but hasn't paid yet |
Orderline States
Individual orderlines within an order have their own states:
| State | Description |
| ----- | ------------------------------ |
| 0 | New |
| 1 | Paid (Pending validation) |
| 2 | Validated (Accepted by seller) |
| 3 | Shipped |
| 4 | Completed (Delivered) |
| 5 | Cancellation requested |
| 6 | Cancelled/Refunded |
| 7 | Rejected by buyer |
| 8 | Not paid |
| 9 | Waiting for action |
Listing Publication States
| State | Description |
| ----- | ---------------------- |
| 0 | Not online |
| 1 | Online |
| 2 | Waiting for validation |
| 3 | Rejected |
| 4 | Pending |
API Reference
Using the BackMarket Client
The component provides a BackMarket client class for a more ergonomic API:
import { BackMarket } from "@maxime-ns/back-market";
import { components } from "./_generated/api";
const backMarket = new BackMarket(components.backMarket);Connection Methods
| Method | Context | Description |
| ----------------------- | -------- | ------------------------------------------ |
| getConnectionStatus() | Query | Get connection status (no secrets exposed) |
| upsertConnection() | Mutation | Create or update a connection |
| disconnect() | Mutation | Disable a connection |
| reconnect() | Mutation | Re-enable a previously disabled connection |
| removeConnection() | Mutation | Permanently delete a connection |
Order Methods
| Method | Context | Description |
| ---------------- | ------- | -------------------------------------- |
| getOrders() | Query | Get orders from local database |
| getOrderById() | Query | Get a single order by its ID |
| syncOrders() | Action | Manually sync orders to local database |
| updateOrder() | Action | Update order state, tracking, etc. |
Shipping an Order
// Ship an order with tracking info
const result = await backMarket.updateOrder(ctx, {
organizationId: "org_123",
orderId: 5978,
body: {
new_state: 3, // Shipped
tracking_number: "1Z999AA10123456784",
tracking_url: "https://tracking.example.com/1Z999AA10123456784",
shipper: "UPS",
},
environment: "production",
});Cancelling/Refunding an Order
const result = await backMarket.updateOrder(ctx, {
organizationId: "org_123",
orderId: 5978,
body: {
new_state: 6, // Cancelled/Refunded
return_reason: 0, // Stock mistake
return_message: "Item out of stock",
},
environment: "production",
});Orderline Methods
| Method | Context | Description |
| -------------------------- | ------- | ----------------------------- |
| getOrderlines() | Query | Get orderlines for an order |
| getOrderlineById() | Query | Get a single orderline by ID |
| getOrderlinesByListing() | Query | Get orderlines by SKU/listing |
| getOrderlinesByState() | Query | Get orderlines by state |
| updateOrderline() | Action | Update orderline (e.g., IMEI) |
Updating IMEI on an Orderline
// Update IMEI for a smartphone orderline
const result = await backMarket.updateOrderline(ctx, {
organizationId: "org_123",
orderlineId: 12345,
orderId: 5978,
body: {
imei: "153124587625348",
},
environment: "production",
});
if (result.success) {
console.log(`IMEI updated to: ${result.imei}`);
}Invoice Methods
| Method | Context | Description |
| ---------------------- | ------- | ---------------------------------- |
| uploadOrderInvoice() | Action | Upload custom invoice PDF to order |
Uploading an Invoice
// Get a signed URL for the invoice from your app's storage
const invoiceUrl = await ctx.storage.getUrl(invoiceStorageId);
// Upload to Back Market
const result = await backMarket.uploadOrderInvoice(ctx, {
organizationId: "org_123",
orderId: 5978,
invoiceUrl,
filename: "invoice-5978.pdf",
environment: "production",
});Listing Methods
| Method | Context | Description |
| --------------------------------- | ------- | ------------------------------------------ |
| getListings() | Query | Get listings from local database |
| getListingById() | Query | Get a single listing by UUID |
| getListingBySku() | Query | Get a single listing by SKU |
| syncListings() | Action | Sync listings to local database |
| updateListing() | Action | Update a single listing (price, qty, etc.) |
| createOrUpdateListings() | Action | Bulk create/update via CSV |
| getTaskStatus() | Action | Check status of bulk operation |
| createOrUpdateListingsAndSync() | Action | Bulk update + wait + sync (all-in-one) |
Updating a Single Listing
// Update stock quantity
const result = await backMarket.updateListing(ctx, {
organizationId: "org_123",
listingId: "7a97a1b6-547e-4718-bc72-5b84aca91b09",
body: {
quantity: 50,
price: "199.99",
currency: "EUR",
},
environment: "production",
});
// Take listing offline (set quantity to 0)
await backMarket.updateListing(ctx, {
organizationId: "org_123",
listingId: "7a97a1b6-547e-4718-bc72-5b84aca91b09",
body: { quantity: 0 },
environment: "production",
});Bulk Update Listings via CSV
// Create CSV content
const csvContent = `sku,quantity,price
MY_SKU_1,50,123.45
MY_SKU_2,25,99.99`;
// All-in-one: submit, wait for completion, sync
const result = await backMarket.createOrUpdateListingsAndSync(ctx, {
organizationId: "org_123",
body: {
catalog: csvContent,
quotechar: '"',
delimiter: ",",
encoding: "utf-8",
},
maxWaitMs: 10 * 60 * 1000, // 10 minutes
pollIntervalMs: 15 * 1000, // 15 seconds
environment: "production",
});
if (result.success) {
console.log(`Synced ${result.syncResult?.totalListings} listings`);
}BackShip Delivery Methods
| Method | Context | Description |
| -------------------------- | ------- | -------------------------------------- |
| getDeliveries() | Query | Get deliveries from local database |
| getDeliveryById() | Query | Get a single delivery by shipment ID |
| getDeliveriesByOrderId() | Query | Get all deliveries for an order |
| getLabelUrl() | Query | Get signed URL for shipping label |
| syncDeliveries() | Action | Sync deliveries + download labels |
| downloadLabel() | Action | Download a specific shipping label |
| fetchDelivery() | Action | Fetch single delivery from API + label |
Syncing Deliveries
const result = await backMarket.syncDeliveries(ctx, {
organizationId: "org_123",
downloadLabels: true, // default: true
environment: "production",
});
console.log(`Synced ${result.totalDeliveries} deliveries`);
console.log(`Downloaded ${result.labelsDownloaded} labels`);Getting a Shipping Label URL
const result = await backMarket.getLabelUrl(ctx, {
organizationId: "org_123",
shipmentId: 12345,
});
if (result.success) {
// Open or download the label PDF
window.open(result.url);
}BackShip Return Methods
| Method | Context | Description |
| --------------------------- | ------- | ------------------------------------ |
| getReturns() | Query | Get returns from local database |
| getReturnById() | Query | Get a single return by shipment ID |
| getReturnsByOrderId() | Query | Get all returns for an order |
| getReturnsByOrderlineId() | Query | Get all returns for an orderline |
| getReturnLabelUrl() | Query | Get signed URL for return label |
| syncReturns() | Action | Sync returns + download labels |
| downloadReturnLabel() | Action | Download a specific return label |
| fetchReturn() | Action | Fetch single return from API + label |
Syncing Returns
const result = await backMarket.syncReturns(ctx, {
organizationId: "org_123",
downloadLabels: true,
environment: "production",
});
console.log(`Synced ${result.totalReturns} returns`);
console.log(`Downloaded ${result.labelsDownloaded} labels`);Automatic Data Sync
The component includes cron jobs that automatically sync data every 5 minutes for all enabled connections:
- Orders & Orderlines - Synced together automatically
- Listings - Synced automatically
You can still trigger manual syncs with syncOrders, syncListings,
syncDeliveries, and syncReturns if needed.
Database Schema
The component creates these tables in its namespace:
connections
| Field | Type | Description |
| ---------------------- | --------- | ------------------------------------------ |
| organizationId | string | Unique identifier for the organization |
| enabled | boolean | Whether the connection is active |
| apiKey | string? | Back Market API key (stored securely) |
| lastOrdersSyncAt | number? | Timestamp of last successful order sync |
| lastListingsSyncAt | number? | Timestamp of last successful listings sync |
| lastDeliveriesSyncAt | number? | Timestamp of last deliveries sync |
| lastReturnsSyncAt | number? | Timestamp of last returns sync |
orders
| Field | Type | Description |
| ------------------ | ---------- | --------------------------------- |
| organizationId | string | Organization that owns this order |
| orderId | number? | Back Market order ID |
| state | number? | Order state (0, 1, 3, 8, 9, 10) |
| countryCode | string? | Country code (e.g., "fr-fr") |
| dateCreation | string? | Order creation date |
| dateModification | string? | Last modification date |
| dateShipping | string? | Shipping date |
| trackingNumber | string? | Package tracking number |
| trackingUrl | string? | Package tracking URL |
| shipperDisplay | string? | Shipper name |
| isBackship | boolean? | Whether using BackShip |
| shippingAddress | object? | Customer shipping address |
| billingAddress | object? | Customer billing address |
| lastSyncedAt | number? | When this order was last synced |
orderlines
| Field | Type | Description |
| ---------------- | --------- | ---------------------------------- |
| organizationId | string | Organization that owns this record |
| orderId | number | Parent order ID |
| orderlineId | number | Back Market orderline ID |
| state | number? | Orderline state (0-9) |
| listing | string? | SKU/listing identifier |
| product | string? | Product name |
| quantity | number? | Quantity ordered |
| price | string? | Price per item |
| currency | string? | Currency code |
| imei | string? | IMEI (for smartphones) |
| serialNumber | string? | Serial number |
| lastSyncedAt | number? | When this was last synced |
listings
| Field | Type | Description |
| ------------------ | --------- | ----------------------------------- |
| organizationId | string | Organization that owns this listing |
| listingId | string? | Back Market listing UUID |
| sku | string? | Merchant SKU |
| title | string? | Product title |
| price | string? | Current price |
| currency | string? | Currency code |
| quantity | number? | Available stock |
| publicationState | number? | Publication state (0-4) |
| state | number? | Condition state |
| grade | string? | Grade (e.g., "A", "B") |
| lastSyncedAt | number? | When this was last synced |
deliveries
| Field | Type | Description |
| ---------------- | ---------- | ------------------------------- |
| organizationId | string | Organization that owns this |
| shipmentId | number | Back Market shipment ID |
| orderId | number | Related order ID |
| orderlines | number[] | Related orderline IDs |
| carrierName | string? | Carrier name |
| trackingNumber | string? | Tracking number |
| trackingUrl | string? | Tracking URL |
| labelUrl | string? | Label URL from Back Market |
| labelStorageId | string? | Convex storage ID for label PDF |
| lastSyncedAt | number? | When this was last synced |
returns
| Field | Type | Description |
| ---------------- | --------- | ------------------------------- |
| organizationId | string | Organization that owns this |
| shipmentId | number | Back Market shipment ID |
| orderId | number | Related order ID |
| orderlineId | number | Related orderline ID |
| carrierName | string? | Carrier name |
| trackingNumber | string? | Tracking number |
| trackingUrl | string? | Tracking URL |
| label | string? | Return label URL |
| labelStorageId | string? | Convex storage ID for label PDF |
| lastSyncedAt | number? | When this was last synced |
Connection Lifecycle
The component supports a full connection lifecycle:
┌─────────────────┐
│ No Connection │
└────────┬────────┘
│ upsertConnection()
▼
┌─────────────────┐
│ Connected │◄───────────────┐
│ (enabled) │ │
└────────┬────────┘ │
│ disconnect() │ reconnect()
▼ │
┌─────────────────┐ │
│ Disconnected │────────────────┘
│ (disabled) │
└────────┬────────┘
│ removeConnection()
▼
┌─────────────────┐
│ Deleted │
└─────────────────┘- upsertConnection: Create a new connection or update an existing one
- disconnect: Disable the connection (optionally clear the API key)
- reconnect: Re-enable a disabled connection (if API key is still stored)
- removeConnection: Permanently delete the connection and all its data
Environment Configuration
The component supports both sandbox and production environments:
// Use sandbox for testing
await backMarket.syncOrders(ctx, {
organizationId: "org_123",
environment: "sandbox",
});
// Use production for real data
await backMarket.syncOrders(ctx, {
organizationId: "org_123",
environment: "production",
});Typed API Client
The component also exports a typed API client for direct Back Market API access:
import { createBackmarketClient } from "@maxime-ns/back-market";
// Create a client with your API key
const client = createBackmarketClient({
apiKey: "your-api-key",
environment: "sandbox",
locale: "en-US",
});
// Make typed API calls
const { data, error } = await client.GET("/ws/orders", {
params: {
query: { state: 1, page: 1 },
},
});This client is generated from the Back Market OpenAPI spec and provides full type safety.
Example App
Check out the full example app in the example/ directory:
git clone https://github.com/Maxime-NS/backmarket-component
cd backmarket-component
npm install
npm run devThe example includes:
- Connection management UI
- Order listing with filters
- Manual sync functionality
- Environment switching
Troubleshooting
Orders not syncing
Verify the connection is enabled:
const status = await backMarket.getConnectionStatus(ctx, { organizationId: "your-org-id", }); console.log(status); // Check enabled: trueCheck if the API key is valid by trying a manual sync:
const result = await backMarket.syncOrders(ctx, { organizationId: "your-org-id", environment: "sandbox", }); console.log(result); // Check for errors
"No connection found" errors
Make sure you've created a connection first:
await backMarket.upsertConnection(ctx, {
organizationId: "your-org-id",
apiKey: "your-api-key",
});"Connection is disabled" errors
Re-enable the connection:
await backMarket.reconnect(ctx, {
organizationId: "your-org-id",
});BackShip labels not downloading
- Ensure
downloadLabelsis not set tofalsein sync calls - Check that the delivery/return has a
labelUrlfrom Back Market - Use
downloadLabel()ordownloadReturnLabel()to manually download
Contributing
Found a bug? Feature request? File it here.
See CONTRIBUTING.md for development setup.
License
Apache-2.0
