Skip to main content

Product Sync

Deals Products API →

A standalone API. You call POST /products/sync whenever your catalog changes. Each affected product is one entry in the products array, and each entry is resolved independently.

POST https://store.billease.ph/be-store-admin-api/products/sync
Authorization: Bearer <your-token>
Content-Type: application/json
{
"shop_code": "3c9b1f2a-7d5e-4c11-8a6f-2b9d0e7c1a42",
"mer_code": "8f1c2b7a-3e5d-4c11-9a6f-2b9d0e7c1b13",
"products": [
{ "id": "1000001", "in_stock": 12 },
{ "id": "1000002", "name": "Acme Tab 11 Wi-Fi 128GB" },
{ "id": "1000003", "status": 2 },
{ "id": "1000004", "status": 1 }
]
}

There is no scheduled pull on our side. If you do not call, nothing changes.

Product sync flow

The four operations

There is no operation field; we infer what you meant from the fields you send.

OperationWhat you sendApproval
New productThe complete product object, with an id we have not seenPending approval
Updateid + only the fields to change, no statusInstant for stock only; otherwise pending
Archiveid + status: 2, nothing elseInstant
Unarchiveid + status: 1, nothing elseInstant
id must be stable

We key everything on id and map it to our internal sku. If an id changes (a leading zero appears, or a type coercion turns 1000001 into 1000001.0), we treat it as a brand-new product, create a duplicate, and silently stop updating the original.

Treat id as immutable, and never reuse one.

Approval

New products, and any edit other than a pure stock change, create a pending version. Your currently-live version stays up until we approve the new one.

ChangeApplied
in_stock aloneInstantly
status: 1 / status: 2Instantly
Price, name, description, category, device specs, images, taglines, linksPending approval

The sync response tells you a pending version was created. It does not tell you what happened to it afterwards. For that, poll GET /products:

GET /products?shop_code=<uuid>&status=3
statusMeaning
1Active / approved (the live version)
2Archived
3Pending our approval
4Rejected; see message

You only ever send 1 or 2. 3 and 4 are ours, and appear only on the read endpoint.

Updates: absent vs null

On an update, omit what you are not changing:

{ "id": "1000001", "in_stock": 12 }
You sendWe do
Field absentLeave it unchanged
Field = nullClear it
Check your JSON serializer

A serializer that emits null for unset fields will wipe every optional field on the product. This is the default in several common libraries:

  • Jackson: add @JsonInclude(JsonInclude.Include.NON_NULL)
  • Go encoding/json: add ,omitempty to the struct tags
  • Pydantic: use .model_dump(exclude_none=True)

Send a single-product update to staging and read it back with GET /products before you run a full sync.

To replace the image set, send the complete image_url array; it is not merged.

Categories and device specs

category must be one of the values we recognize, case-sensitive, and each category permits its own set of keys inside device.

Do not hardcode the list. Read it at build time:

GET /categories
{
"categories": [
{
"category": "Mobile phone",
"device_keys": ["device_brand", "device_color", "device_camera", "device_display",
"device_processor", "device_memory", "device_storage", "device_os"]
},
{ "category": "Vouchers", "device_keys": ["device_brand", "device_merchant"] }
]
}

Device keys outside a category's list are not recognized and are dropped; the rest of the product is stored. An unrecognized category, however, fails the whole call with a 400 Bad Request (the batch is atomic, so nothing is applied); the body names the product:

{ "message": "product 2000100501 category not found: Invalid-name-category" }

Send only the categories and keys that GET /categories returns.

Images

{
"url": "https://cdn.your-company.com/p/1000900/main.jpg",
"hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"order": 0
}
  • url must be publicly reachable and stay reachable while the product is live.
  • hash is the lowercase hex SHA-256 of the raw image bytes. We compare it against the stored value to decide whether to re-download, so it has to be computed the same way on both sides.
  • order is 0-based. The image at order: 0 is the thumbnail.

At least one image is required; two or more is recommended.

Reading the response

The call is atomic. Either every entry passes validation and you get 202 Accepted, or the whole batch is rejected with a 4xx and nothing is applied. We never process part of a batch.

A 202 returns one result per submitted product, in request order:

{
"results": [
{ "id": "1000001", "sku": 50231, "operation": "update", "applied": "instant" },
{ "id": "1000002", "sku": 50232, "operation": "update", "applied": "pending_approval" }
]
}

applied is instant (already live) or pending_approval (a pending version was created; poll GET /products for the outcome).

Error responses

On failure you get one of the statuses below with a JSON body that names the problem, e.g. { "message": "product 2000100501 category not found: Invalid-name-category" }. Fix it and resend the whole batch.

StatusMeaning
400Malformed request, or a product with an invalid/unsupported combination of fields: a status other than 1 or 2, an unrecognized category, a missing required field, or deals_price above retail_price.
401Missing or invalid JWT.
403The JWT is valid, but the shop_code / mer_code does not belong to you.
404An update / archive / unarchive references an id we do not have on record.
409 / 422Validation failure on a new product (e.g. a duplicate name).
413More than 1000 entries in one call. Split the batch.
429Too many requests. Back off and retry.
500Unexpected error on our side. Safe to retry.
Check the status, not just the body

Because the batch is atomic, a 2xx means the entire batch was accepted and any 4xx means none of it was. A pipeline that ignores the status code will treat a rejected batch as a clean sync.

Limits

  • Maximum 1000 entries per call. Larger batches return 413.
  • 100 requests per second per endpoint. On 429, back off and retry.

For an initial catalog load, page through in batches of 1000 and check the results of each before sending the next.