Skip to content

Webhooks

Mehrdad Pajuhaan edited this page Oct 23, 2026 · 1 revision

Webhooks

Selldone calls your bridge's webhook URLs to inform you about updates and to fetch information about connected accounts. This page explains how the sample verifies each call, which fields each webhook receives and what the sample returns. Handlers live in app/Http/Controllers/WebhooksController.php; parsing and validation live in selldone/provider/webhook.

Headers

Selldone sends three headers with every webhook call:

Header Meaning
Selldone-Sign-Id A unique ID for each request: the UTC timestamp. You can check it to reject duplicated old requests.
Selldone-Sign-Hash A Bcrypt hash of {sign key}:{Selldone-Sign-Id}.
Authorization The access token your service issued during authentication, e.g. Bearer <token>. It tells you which account on your service the call is for.

Signature verification

Selldone\provider\webhook\Webhooks verifies every call before any handler runs:

$sign_id   = $request->header('Selldone-Sign-Id');
$sign_hash = $request->header('Selldone-Sign-Hash');

if (!$sign_id || !$sign_hash || !Hash::check($this->webhook_sign_key . ":$sign_id", $sign_hash)) {
    throw new Exception("Invalid signature!", 403);
}

Hash::check is Laravel's Bcrypt check. The sign key comes from SD_PROVIDER_WEBHOOK_SIGN_KEY.

Handler pattern

Every handler in the sample follows the same three steps:

// 1. Initialize and verify the webhook
$providerClient = self::newProviderClient();
try {
    $webhook = $providerClient->webhook($request)->onSyncProducts();
} catch (Exception $e) {
    return ErrorResponse::Exception($e);   // status = exception code (403/400) or 500
}

// 2. Check the access token
if ($webhook->getToken() != 'Bearer ' . Controller::SAMPLE_ACCESS_TOKEN) {
    return response("Invalid access token!", 403);
}

// 3. Build the response
return $webhook->response()->addProduct($product)->setTotal(1)->toArray();
Failure Status Body
Missing or invalid signature 403 Invalid signature!
Missing Authorization header 403 Authentication token does not exist on the header!
Payload validation fails 400 Validation messages
Token does not match 403 Invalid access token!

Webhook reference

Webhook Route in sample Parser (on…())
Notify shop POST /api/shop onNotifyShop() → WebhookNotifyShop
Fetch categories GET /api/categories onSyncCategories() → WebhookSyncCategories
Fetch products GET /api/products onSyncProducts() → WebhookSyncProducts
Create order POST /api/create-order onCreateOrder() → WebhookCreateOrder
Confirm order manually POST /api/confirm-order onConfirmOrder() → WebhookConfirmOrder
Get order status GET /api/get-order onGetOrder() → WebhookGetOrder
Cancel order POST /api/cancel-order onCancelOrder() → WebhookCancelOrder

Notify shop

Called when a shop connects your provider or removes it from its Connect OS.

Field Rule
action required, one of Add, Remove, Test

Sample responses:

  • Test — a list of name/value rows shown to the merchant:

    [
      { "name": "Categories count", "value": 12 },
      { "name": "Products count", "value": 85 },
      { "name": "Status", "value": "Active" },
      { "name": "Message", "value": "Some note..." }
    ]
  • Add / Remove — { "success": true }

Fetch categories

Used to fetch categories from your service and update linked categories on Selldone, each time a new shop connects or a sync is triggered.

Field Rule
offset required integer, min 0
limit required integer, min 1
shop_id required integer, min 0

Build the response with SyncCategoriesResponse:

return $webhook->response()
    ->addCategory(new WebhookCategory('sample-sync', 'Sample Sync', 'Some note...', null, null))
    ->setTotal(1)
    ->toArray();   // { "categories": [...], "total": 1 }

WebhookCategory($name, $title, $description, $icon, $category) — category is the parent category name.

Fetch products

Used to fetch products from your service and update linked products on Selldone.

Field Rule
offset required integer, min 0
limit required integer, min 1
shop_id required integer, min 0

Build the response with SyncProductsResponse, which returns { "override": false, "override_price": false, "products": [...], "total": n }. override forces Selldone to overwrite non-pricing values; override_price forces it to overwrite prices.

$product = new WebhookProduct("1001", ProductType::PHYSICAL, 59, 'USD',
    "iPhone 14 Pro and iPhone 14 Pro Max", "...", 10,
    "https://selldone.com/images/weekly-reports/1.jpg");

$product->setDiscount(5, Carbon::now()->subDay(), Carbon::now()->addMonth())
    ->addImages("https://.../40.jpg", "https://.../41.jpg")
    ->setStyle(false)
    ->addVariants(
        new WebhookVariant("1001-1", '#0097A7', null, "Small", null, null, null, 10, "https://.../42.jpg"),
        new WebhookVariant("1001-2", '#AFB42B', null, "Small", null, null, null, 10, "https://.../43.jpg"),
    );

return $webhook->response()->addProduct($product)->setTotal(1)->toArray();
  • WebhookProduct($mpn, $type, $price, $currency, $title, $title_en, $quantity, $icon) — mpn is your unique product ID. Setters include setCategory, setSku, setGtin, setGpc, setBrand, setWarranty, setSpec, setPros, setCons, setMessage, setInputs, setOutputs, setQuantity, setLead, setExtra, setStyle, setReturnWarranty, setOriginal, setCondition, setVideo, setPriceInput, setCommission, setPriceLabel, setStatus.
  • WebhookVariant($mpn, $color, $style, $volume, $weight, $pack, $type, $quantity, $image) — mpn is your unique variant ID; Selldone stores it in the variant's MPN and sends it back in order items.
  • Enums in selldone/provider/enums: ProductType (PHYSICAL, VIRTUAL, SERVICE, FILE), ProductStatus (Open, Close), ProductCondition (new, refurbished, used, used_fair, used_good, used_like_new), PriceInputType (default, area, volume, custom), ProductPricing (FIX, ESTIMATION, AGREEMENT, BID).

Create order

Called when a new order is created, or when the merchant manually asks to sync an order with your service.

Field Rule / content
shop_id required integer — reference shop ID
id required integer — fulfillment ID on Selldone (a basket can be split into several fulfillments, one per provider)
basket_id required integer — basket reference ID (do not use it as a unique order identifier)
confirm optional boolean — auto-confirm flag, based on the provider's configuration
recipient required object — name, company, address1, address2, city, state_code, state_name, country_code, zip, phone, email, tax_number, lng, lat, message
items required array — each item has index, mpn, quantity, id, basket_id, product_id, variant_id, retail_price, name, sku. Use mpn and quantity to identify what was bought.
retail_costs required object — currency (3–4 chars), subtotal, discount, shipping, tax, total (required)
gift optional object — subject, message
packing_slip optional object — email, phone, logo_url, store_name, custom_order_id
fulfillment required object — the reference fulfillment on Selldone

Respond with an UpdateOrderResponse:

$response = $webhook->response(165, 'USD')          // your price and currency
    ->setRetailCosts('USD', 200, 10, 10, 0, 0, 0, 20, 220)
    ->setServiceCosts('USD', 120, 20, 10, 0, 30, 15, 0, 165);

if ($webhook->confirm) {
    $response->setConfirmAt(Carbon::now());          // auto confirm
}
return $response;

setRetailCosts / setServiceCosts take (currency, subtotal, discount, shipping, additional_fee, fulfillment_fee, tax, vat, total).

Confirm order

Called when the merchant confirms an order manually.

Field Rule
shop_id, id, basket_id required integers
action required, must be Confirm
fulfillment required object

The sample returns the costs and setConfirmAt(Carbon::now()).

Get order

Returns the last status of the order.

Field Rule
shop_id, id, basket_id required integers

The sample returns costs, a carrier and tracking URL (setCarrier('Fedex', 'https://fedex.com/track/...')) and a confirmation date.

Cancel order

Called when the order is canceled. It is only called if the order was confirmed before.

Field Rule
shop_id, id, basket_id required integers
fulfillment required object

The sample returns costs, carrier, setConfirmAt(...) and setCancelAt(Carbon::now()).

Order response fields

UpdateOrderResponse is serialized as JSON with: id, price, currency, and — when set — confirm_at, cancel_at, fulfill_at, service_costs, retail_costs, carrier, tracking_url, error.

Related

Clone this wiki locally