Webhooks: Add/Update Product
Overview
The Add/Update Product webhook allows Talk2sync to create new products or update existing ones in your system. Talk2sync sends a POST request with complete product data that your application must insert or update in your database.
This endpoint implements an upsert operation (insert or update) where Talk2sync either creates a new product or updates an existing one based on the product ID.
Request Format
HTTP Request
POST https://www.example.com/your/endpoint/url?jobid=123&id=999999
T2SKey: {{API_KEY}}
Content-Type: application/json
{
"_id": "999999",
"sku": "999999",
"last_updated": 1517360038797,
"title": "EON618S JBL SUBWOOFER 18\" AMPLIFICADO",
"url": "",
"brand": "",
"mpn": "",
"model": "",
"description": "JBL Premium Transducers",
"variations": [
{
"availabilities": [
{
"tag": "default",
"quantity": 50
}
],
"prices": [
{
"tag": "default",
"currency": "USD",
"number": 1060.51
}
],
"images": [
{
"url": "https://www.example.com/13080-thickbox_default/eon618s-jbl-subwoofer-18-amplificado-.jpg"
}
],
"videos": [
{
"url": ""
}
],
"barcode": "",
"size": "",
"color": "",
"variationid": ""
}
],
"properties": [
{
"extraattributes": {
"promociones": "",
"status": "",
"condicion_venta": "",
"peso": "",
"relacionados": "",
"custom_01": "",
"custom_02": "1",
"custom_03": ""
}
}
]
}
Request Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
jobid | integer | Unique identifier for this sync job (for tracking) | ✓ Yes |
id | string | Product ID to insert/update | ✓ Yes |
T2SKey | header | Your security key | ✓ Yes |
Request Body
The request body contains the complete product object with all details. Refer to List Products for a complete field reference.
Success Response
If the product is successfully created or updated, return a 200 OK response with the following JSON structure:
{
"success": true,
"code": 200,
"warnings": [],
"product": {
"_id": "100004777",
"sku": "100004777",
"last_updated": 1517360038797,
"title": "EON618S JBL SUBWOOFER 18\" AMPLIFICADO",
"url": "",
"brand": "",
"mpn": "",
"model": "",
"description": "JBL Premium Transducers",
"variations": [
{
"availabilities": [
{
"tag": "default",
"quantity": 50
}
],
"prices": [
{
"tag": "default",
"currency": "USD",
"number": 1060.51
}
],
"images": [
{
"url": "https://www.example.com/13080-thickbox_default/eon618s-jbl-subwoofer-18-amplificado-.jpg"
}
],
"videos": [
{
"url": ""
}
],
"barcode": "",
"size": "",
"variationid": ""
}
],
"properties": [
{
"extraattributes": {
"promociones": "",
"status": "",
"condicion_venta": "",
"peso": "",
"relacionados": "",
"custom_01": "",
"custom_02": "1",
"custom_03": ""
}
}
]
}
}
Success Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Always true for successful operations |
code | integer | HTTP status code (e.g., 200) |
warnings | array | Array of warning messages (if any non-critical issues) |
product | object | The successfully created/updated product object |
Warnings Example
You can include non-critical warnings if certain fields were adjusted:
{
"success": true,
"code": 200,
"warnings": [
{
"msg": "Image URL was invalid and has been skipped"
},
{
"msg": "Price was adjusted to match your store's currency"
}
],
"product": { ... }
}
Error Response
If the operation fails, return an error response with detailed information about what went wrong:
{
"success": false,
"code": 400,
"error": "Could not upsert product",
"reasons": [
{
"msg": "Invalid price zero"
},
{
"msg": "Invalid repeated title"
},
{
"msg": "Invalid images"
}
],
"warnings": []
}
Error Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Always false for failed operations |
code | integer | HTTP status code (e.g., 400, 500) |
error | string | High-level error message |
reasons | array | Array of specific error reasons |
warnings | array | Any warnings that occurred before the failure |
Common Error Scenarios
Validation Errors (400 Bad Request)
{
"success": false,
"code": 400,
"error": "Validation failed",
"reasons": [
{
"msg": "Title is required"
},
{
"msg": "Price must be greater than zero"
}
],
"warnings": []
}
Database Errors (500 Internal Server Error)
{
"success": false,
"code": 500,
"error": "Database operation failed",
"reasons": [
{
"msg": "Could not connect to database"
}
],
"warnings": []
}
Authentication Error (401 Unauthorized)
{
"success": false,
"code": 401,
"error": "Unauthorized",
"reasons": [
{
"msg": "Invalid or missing API key"
}
],
"warnings": []
}
Implementation Example (Node.js/Express)
app.post('/api/products', (req, res) => {
const { jobid, id } = req.query;
const apiKey = req.headers['t2skey'];
const productData = req.body;
// Validate API key
if (apiKey !== process.env.TALK2SYNC_KEY) {
return res.status(401).json({
success: false,
code: 401,
error: 'Unauthorized',
reasons: [{ msg: 'Invalid or missing API key' }],
warnings: []
});
}
try {
// Validate product data
const validationErrors = validateProduct(productData);
if (validationErrors.length > 0) {
return res.status(400).json({
success: false,
code: 400,
error: 'Validation failed',
reasons: validationErrors.map(msg => ({ msg })),
warnings: []
});
}
// Perform upsert operation
const savedProduct = upsertProduct(id, productData);
// Return success response
res.json({
success: true,
code: 200,
warnings: [],
product: savedProduct
});
} catch (error) {
console.error('Error upserting product:', error);
res.status(500).json({
success: false,
code: 500,
error: 'Database operation failed',
reasons: [{ msg: error.message }],
warnings: []
});
}
});
// Validation function
function validateProduct(product) {
const errors = [];
// Check required fields
if (!product.title || product.title.trim() === '') {
errors.push('Title is required');
}
// Validate variations
if (!product.variations || product.variations.length === 0) {
errors.push('At least one variation is required');
}
// Validate prices
product.variations?.forEach((variation, idx) => {
variation.prices?.forEach((price, priceIdx) => {
if (price.number <= 0) {
errors.push(`Variation ${idx}: Price ${priceIdx} must be greater than zero`);
}
});
});
// Validate images
product.variations?.forEach((variation, idx) => {
variation.images?.forEach((image, imgIdx) => {
if (!isValidUrl(image.url)) {
errors.push(`Variation ${idx}: Image ${imgIdx} has invalid URL`);
}
});
});
return errors;
}
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
// Upsert function
function upsertProduct(id, productData) {
// Check if product exists
const existingProduct = findProductById(id);
if (existingProduct) {
// Update existing product
return updateProductInDatabase(id, productData);
} else {
// Insert new product
return insertProductInDatabase(productData);
}
}
Response Status Codes
| Status | Meaning | Response | Action |
|---|---|---|---|
200 OK | Product created/updated successfully | Success response with product | Talk2sync records the operation |
400 Bad Request | Validation error in product data | Error response with reasons | Check validation errors |
401 Unauthorized | Invalid or missing API key | Error response | Verify your security key |
409 Conflict | Product conflicts with existing data | Error response with reasons | Resolve the conflict |
500 Internal Server Error | Server error | Error response with error message | Check your server logs |
Best Practices
✓ Do:
- Validate all required fields before insertion/update
- Return specific error messages in the
reasonsarray - Include warnings for non-critical issues
- Always validate the API key
- Use transactions for database operations (if supported)
- Update the
last_updatedtimestamp to current time - Log all operations for debugging
✗ Don't:
- Accept partial product data (require complete objects)
- Silently skip invalid fields
- Return vague error messages
- Modify data that wasn't sent by Talk2sync
- Update products that don't belong to the current account/store
- Ignore validation errors
Upsert Logic
The upsert operation should work as follows:
IF product with ID exists in database
THEN update all fields with new data
ELSE
CREATE new product with provided data
END
Create vs Update
| Operation | When | Action |
|---|---|---|
| CREATE (INSERT) | Product ID doesn't exist | Insert a new product record |
| UPDATE | Product ID already exists | Replace all fields with new data |
Data Consistency
- Timestamps: Always use the
last_updatedtimestamp from Talk2sync (don't generate a new one) - Product ID: The
_idandidparameter should match - SKU: Store the SKU for reference; it should match the
_idin most cases - Relationships: Clear any old variations/properties before updating
Idempotency
This operation is idempotent, meaning calling it multiple times with the same data should produce the same result:
- First call: Creates/updates the product
- Second call (same data): Returns the same success response
- Third call (same data): Returns the same success response
This ensures that if Talk2sync retries the operation, no duplicate products are created.
Request/Response Size Limits
- Maximum request size: 10 MB per product
- Response should be kept under 5 MB
- Process requests within 1 minute timeout