Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The Authentication API serves as the primary gateway for secure access to LiquidCommerce Services
The LiquidCommerce Authentication API provides a secure method to obtain an access token for all other API calls to LiquidCommerce Services. This token ensures secure access to various LiquidCommerce services and enables seamless integration into applications.
Obtain Access Tokens: Retrieve a token for API authentication.
Secure API Access Management: Safeguard API access through token-based authentication.
Service Integration: Integrate LiquidCommerce services into your applications efficiently.
These URLs serve as the base for all LiquidCommerce API calls, depending on the environment (staging or production).
Staging: https://staging.api.liquidcommerce.cloud
Production: https://api.liquidcommerce.cloud
After obtaining the , include it in the Authorization header of subsequent API calls to other LiquidCommerce services.
Initial Authentication: Exchange API key for access token
Token Usage: Include token in Authorization header
Token Refresh: Use refresh mechanism before expiration
Token Invalidation: Automatic expiry
All API endpoints that require authentication now support token refreshing. This is done by including a refresh parameter in the request body.
When refresh is set to true, the API response will include an auth object containing the new access token information.
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The Authentication API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
Store the access token securely on the client-side.
Include the refresh parameter when making requests close to the token's expiration time.
Update the stored access token whenever a new one is received in the auth object of a response.
refresh
boolean
When set to true, a new access token will be generated and returned
Authorization: Bearer <YOUR_ACCESS_TOKEN>{
"auth": {
"token": "<NEW_ACCESS_TOKEN>",
"type": "ACCESS_TOKEN",
"exp": 1722920383204
},
// ... other response data
}LiquidCommerce offers an enterprise-grade API Services for the beverage alcohol industry with real-time processing and AI-powered e-commerce features
Our APIs offer a comprehensive set of tools for managing product catalogs, processing orders, handling payments, and much more. By leveraging advanced technologies such as AI and real-time data processing, LiquidCommerce APIs enable businesses to create sophisticated, efficient, and personalized shopping experiences.
Highly Scalable Infrastructure: Our APIs are built on a robust, cloud-based infrastructure that dynamically allocates resources to meet growing demand, ensuring consistent performance even during peak times.
Secure Data Management: We implement rigorous security measures to protect sensitive data and transactions, maintaining compliance with industry standards.
Efficient API Interactions: Our comprehensive set of APIs allows for streamlined integration with various services such as address validation, catalog management, and order processing.
LiquidCommerce offers a range of APIs to cover all aspects of e-commerce operations:
Authentication: Securely manage access to LiquidCommerce services.
Address: Validate and standardize shipping addresses.
Users: Handle user account creation, management, and authentication.
Catalog: Access our extensive product catalog with advanced search, filtering, and recommendation capabilities.
These URLs serve as the base for all LiquidCommerce API calls, depending on the environment.
Staging: https://staging.api.liquidcommerce.cloud
Production: https://api.liquidcommerce.cloud
Enhanced Customer Experience: Leverage AI-powered recommendations and real-time inventory data to provide a seamless shopping experience.
Operational Efficiency: Automate and streamline various e-commerce processes, reducing manual work and potential errors.
Scalability: Easily handle increased traffic and transactions as your business grows.
Flexibility: Customize and integrate LiquidCommerce APIs to fit your specific business needs and existing systems.
All monetary amounts are stored as integers representing cents to avoid floating-point precision issues. This ensures accurate financial calculations and prevents rounding errors in currency operations. Examples:
$12.34 is stored as 1234 cents
$0.99 is stored as 99 cents
$100.00 is stored as 10000 cents
Display formatting to dollar amounts should only occur at the presentation layer.
To begin using LiquidCommerce APIs:
Contact our sales team to set up an account and obtain API credentials.
Review our comprehensive API documentation for each specific API.
Use our SDKs or direct API calls to integrate LiquidCommerce services into your application.
Test your integration in our sandbox environment before going live.
For detailed information on each API, including endpoints, request/response formats, and best practices, please refer to the individual API documentation pages.
Unified Payment Processing: Integrated with Stripe, our payment system allows for quick retailer onboarding and supports various payment methods like Credit Cards, Apple Pay, Amazon Pay, Google Pay.
Cart: Manage shopping carts with features like adding/removing items, applying promotions, and calculating totals in real-time.
Checkout: Streamline the checkout process with customizable workflows.
Data-Driven Insights: Access comprehensive data and analytics to make informed business decisions.
Compliance: Stay compliant with industry regulations through our secure and standardized API processes.
The LiquidCommerce Address API provides comprehensive address validation, standardization, and geocoding services optimized for beverage alcohol delivery and shipping
It leverages Google Places API to offer address autocomplete functionality and retrieve geographical coordinates, enhancing the accuracy of shipping addresses and enabling location-based services.
Address autocomplete using Google Places API
Geocoding to obtain latitude and longitude for addresses
Validation of shipping addresses
Support for international addresses
Before using the API, you need to obtain an access token from the . Include this token in the Authorization header of all API requests:
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
Use the autocomplete endpoint to provide real-time suggestions as users type their address.
Always use the place ID returned by the autocomplete endpoint to fetch detailed address information.
Store both the formatted address and the geocoding data (latitude and longitude) for each validated address.
Implement proper error handling to manage cases where address validation fails.
The LiquidCommerce Cart API provides comprehensive cart management capabilities with real-time pricing, inventory verification, and multi-retailer support
The Orders API provides secure access to order data throughout the finalized states of the order lifecycle within the LiquidCommerce ecosystem.
Our system implements two distinct approaches to handle exceptional situations:
created - Order has been successfully created in the system but processing has not yet begun
processing - Order is actively being processed (payment verification, inventory allocation, etc.)
canceled - Order has been canceled and will not be fulfilled
delivered - Order has been successfully delivered to the customer
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
If you have any questions or need assistance with the API, please contact our support team at [email protected]
Strict error handling that prevents invalid checkout operations. When an error occurs during checkout, the operation fails with a specific status code and message to help identify and resolve the issue.
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact [email protected].
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact [email protected].
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact [email protected].
Format: userID:password (Base64 encoded)
Example: Authorization: Basic dXNlcklEOnBhc3N3b3Jk
Receive Access Token:
Upon successful authentication, the server returns an access token
The token has a limited validity period (default: 60 minutes)
Use Access Token:
Include the token in the Authorization header for all subsequent API requests
Format: Authorization: Bearer {access_token}
Access tokens expire after their designated lifetime. When a token expires, you must request a new one using your credentials. Do not store access tokens for extended periods.
Never share your userID and password in client-side code
Store access tokens securely and transmit only over HTTPS
Implement token refresh logic to handle expiration during active sessions
Authorization: Bearer <YOUR_ACCESS_TOKEN>POST order-authentication
Host: staging.api.liquidcommerce.cloud
Authorization: Basic dXNlcklEOnBhc3N3b3Jk
Content-Type: application/json{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600
}Create and update shopping carts
Add, remove, or modify items in the cart
Apply promotional codes
Calculate totals including product prices, shipping fees, and platform fees
Support for multiple retailers and fulfillment methods
Before using the API, you need to obtain an access token from the Authentication API. Include this token in the Authorization header of all API requests:
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The Cart API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
Always include the loc object to ensure accurate pricing and availability.
Use the id field to update an existing cart rather than creating a new one for each update.
Handle the events array in the response to manage any warnings or informational messages.
Regularly update the cart to reflect any changes in product availability or pricing.
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact .
Use this endpoint with your API key to get an access token, which is essential for authenticating other API requests.
GET /authentication
Metadata Object
After obtaining the access token, include it in the Authorization header on any API calls to LiquidCommerce API Services.
All API endpoints that require authentication now support token refreshing. This is done by including a refresh parameter in the request body.
When refresh is set to true, the API response will include an auth object containing the new access token information.
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The Authentication API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
Store the access token securely on the client-side.
Include the refresh parameter when making requests close to the token's expiration time.
Update the stored access token whenever a new one is received in the auth object of a response.
Update the default payment method for a user by ID
The Catalog API provides access to LiquidCommerce's comprehensive beverage alcohol catalog, featuring intelligent search, dynamic filtering, and AI-powered recommendation capabilities
The Catalog API allows you to:
Perform intelligent text searches with AI-powered natural language processing
Apply dynamic filters to refine product results in real-time
Sort results based on various criteria
Retrieve personalized product recommendations
Access content-enhanced searching for recipes and articles
Utilize advanced product tagging for diverse attributes
Enhanced Product Filtering and Sorting: Utilizes LiquidCommerce's real-time data processing for dynamic and accurate product filtering.
Intelligent Product Recommendations: Returns products using a proprietary recommendation engine powered by GCP Retail and Vertex AI, continuously training models for personalized suggestions.
Content Enhanced Searching, Filtering and Sorting: Returns the latest recipes and articles either through keywords or UPCs.
Before using the API, you need to obtain an access token from the . Include this token in the Authorization header of all API requests:
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
Authorization: Bearer <YOUR_ACCESS_TOKEN>data
boolean
Updating the payment to isDefault
path
string
API path
version
string
API version used for the request
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
userId
string
Existing user identifier, returned upon session creation or update
paymentId
string
Default payment identifier of the user
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact [email protected].
ItemQuantityChange: Item quantity reduced from ${originalQuantity} to ${stock} due to limited stock availability
MaxQuantityPerOrderExceeded: The maximum quantity per order for this item is ${maxPresaleQuantity}. Quantity has been adjusted accordingly. || Quantity adjusted from ${originalQuantity} to ${quantity} due to presale availability
RetailerOnDemandHoursNotAvailable: The following cart item(s) have been removed due to retailer hours of operation restrictions
ErrorProcessingGiftCards: There's been an error processing your gift card(s)
InvalidGiftCardCodes: The gift card code(s) you entered is invalid
InvalidGiftCardPartner: This gift card cannot be used with this merchant
InactiveGiftCard: This gift card is currently inactive
GiftCardAlreadyInUse: This gift card has already been used
GiftCardExpired: This gift card has expired
GiftCardBalanceDepleted: This gift card has no remaining balance
RetailerDoesNotAllowPromos: This retailer does not accept promo codes || None of the selected retailers accept promo codes || ${retailerNames} does not accept promo codes || Promo code applied. Note: ${retailerNames} does not accept promo codes || Promo code applied to eligible items only. The following retailers do not accept promo codes: ${retailerNames}
RetailersDoNotAllowPromos: Selected retailers do not accept promo codes || None of the selected retailers accept promo codes || ${retailerNames} does not accept promo codes || Promo code applied. Note: ${retailerNames} does not accept promo codes || Promo code applied to eligible items only. The following retailers do not accept promo codes: ${retailerNames}
RetailerDoesNotAllowGiftCards: Gift card applied. Note: ${retailerNames} does not accept gift cards || Gift card applied to eligible retailers only. The following retailers do not accept gift cards: ${retailerNames}
RetailersDoNotAllowGiftCards: ${retailerNames} does not accept gift cards || None of the selected retailers accept gift cards
curl --location 'https://staging.api.liquidcommerce.cloud/users/payments/updateDefault' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "usrid_123abc456def",
"paymentMethodId": "paymentid_123abc456def",
"isDefault": false
}'{
"statusCode": 200,
"message": "Updating default payment method succeeded",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731600946784,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/payments/updateDefault/usr_123abc456def/paymentid_123abc456def",
"version": "1.7.0"
},
"data": true
}Authorization: Bearer <YOUR_ACCESS_TOKEN>interface ICheckoutEvents {
type: CHECKOUT_EVENT_ENUM;
message: string;
items?: Array<Partial<ICheckoutItem>>;
}enum CHECKOUT_EVENT_ENUM {
ERROR_PROCESSING_GIFT_CARDS = 'ErrorProcessingGiftCards',
INVALID_GIFT_CARD_CODE = 'InvalidGiftCardCodes',
INVALID_GIFT_CARD_PARTNER = 'InvalidGiftCardPartner',
INACTIVE_GIFT_CARD = 'InactiveGiftCard',
GIFT_CARD_ALREADY_IN_USE = 'GiftCardAlreadyInUse',
GIFT_CARD_EXPIRED = 'GiftCardExpired',
GIFT_CARD_BALANCE_DEPLETED = 'GiftCardBalanceDepleted',
RETAILER_ONDEMAND_HOURS_NOT_AVAILABLE = 'RetailerOnDemandHoursNotAvailable',
ITEM_QTY_CHANGE = 'ItemQuantityChange',
MAX_QUANTITY_PER_ORDER_EXCEEDED = 'MaxQuantityPerOrderExceeded',
RETAILER_DOES_NOT_ALLOW_PROMOS = 'RetailerDoesNotAllowPromos',
RETAILERS_DO_NOT_ALLOW_PROMOS = 'RetailersDoNotAllowPromos',
RETAILER_DOES_NOT_ALLOW_GIFT_CARDS = 'RetailerDoesNotAllowGiftCards',
RETAILERS_DO_NOT_ALLOW_GIFT_CARDS = 'RetailersDoNotAllowGiftCards',
}Content-Type
application/json
X-LIQUID-API-KEY
<YOUR_API_KEY>
statusCode
number
Internal status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
data
Authentication object
token
string
The access token to be used for other API calls
type
string enum
The type of token, always "ACCESS_TOKEN"
exp
number
The expiration timestamp of the token (1 day)
refresh
boolean
When set to true, a new access token will be generated and returned
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact [email protected].
data
Array<>
Array of Google Place ID objects
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
input
string
Partial address input for autocomplete suggestions
key
string
Your Google Places API key
refresh
boolean
When set to true, a new access token will be generated and returned
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
id
string
Google Places API location identifier
description
string
An address returned
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
token
string
Checkout token from prepare endpoint
payment
string
Payment method ID or new payment token, you must use the to mount/inject a paymentElement to be able to generate a payment token.
statusCode
number
Internal status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
userId
string
Existing user ID, returned upon session creation or update
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
data.deleted
boolean
States if delete succeded or not
User creation
User management (update user information)
User Addresses
User Saved Payment Methods
Secure handling of Personally Identifiable Information (PII)
PCI DSS compliant data storage and transmission
LiquidCommerce takes the security and privacy of user data very seriously. Our User API is designed with the following security measures:
PCI DSS Compliance: Our systems are fully compliant with the Payment Card Industry Data Security Standard (PCI DSS), ensuring that all sensitive payment information is handled securely.
Secure PII Handling: All Personally Identifiable Information (PII) is encrypted both in transit and at rest using industry-standard encryption protocols.
Data Minimization: We collect and store only the minimum amount of PII necessary for the operation of our services.
Access Control: Strict access controls and authentication mechanisms are in place to ensure that only authorized personnel can access user data.
Regular Audits: We conduct regular security audits and assessments to maintain the highest levels of data protection.
Data Retention: User data is retained only for as long as necessary and in compliance with applicable laws and regulations.
Data Purging: User data is permanently and securely erased using industry-standard deletion methods when no longer needed or upon request, ensuring complete removal from all systems including backups and archives.
Before using the API, you need to obtain an access token from the Authentication API. Include this token in the Authorization header of all API requests:
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
The API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact .
userId
string
Existing user ID, returned upon session creation or update
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
data
User object
Unlike cart events, checkout operations use strict error handling with specific status codes. When an error occurs, the operation fails with a corresponding error code and message.
5480: Default checkout error
5487: Parameter validation error
5488: Tax calculation error
5481: Items out of stock at location
5482: Location mismatch error
5484: Cart no longer available
5485: Invalid cart ID
5486: Issue with cart items
5483: Age verification error
5495: Customer account not found
5489: Invalid checkout token
5490: Default completion error
5491: Status update error
5496: Invalid payment method
5497: Shipping address error
5498: Invalid billing address
5503: Tips application error
The LiquidCommerce Cloud SDK provides an easy way to interact with our APIs through a server-side SDK for Node.js and Web JS script.
curl --location 'https://api.liquidcommerce.cloud/authentication'{
"statusCode": 200,
"message": "OK",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731590739955,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/authentication",
"version": "1.7.0"
},
"data": {
"token": "<YOUR_ACCESS_TOKEN>",
"type": "ACCESS_TOKEN",
"exp": 1731674132795
}
}{
"statusCode": 5000,
"message": "API Key not found",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731592190265,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/authentication",
"version": "1.7.0"
},
"errors": []
}Authorization: Bearer <YOUR_ACCESS_TOKEN>{
"auth": {
"token": "<NEW_ACCESS_TOKEN>",
"type": "ACCESS_TOKEN",
"exp": 1722920383204
},
// ... other response data
}curl --location 'https://staging.api.liquidcommerce.cloud/address/autocomplete' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"input": "100 Madison Ave, New",
"key": "<YOUR_GOOGLE_PLACES_API_KEY>"
"refresh": false"
}'{
"statusCode": 200,
"message": "Get list address according to search.",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731592446922,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/address/autocomplete",
"version": "1.7.0"
},
"data": [
{
"id": "placeid_123abc456ef",
"description": "100 Madison Ave, Morristown, New Jersey, USA"
},
{
"id": "placeid_789abc456ef",
"description": "100 Madison Ave, New York, NY, USA"
}
]
}{
"statusCode": 200,
"message": "Processed your checkout and successfully charged the shopper.",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731629320062,
"timezone": "UTC",
"requestId": "requestid_123abc456def",
"path": "/api/checkout/complete",
"version": "1.7.0"
},
"order": {
"number": "ordernumber_1737134234213"
"referenceId": "referenceid_123abc456def"
}
}curl --location 'https://staging.api.liquidcommerce.cloud/checkout/complete' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN'> \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "preparetoken_123abc456def",
"payment": "paymentid_123abc456def"
}'{
"statusCode": 200,
"message": "Deleting a customer",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731597745591,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/purge/usrid_123abc456def",
"version": "1.7.0"
},
"data": {
"deleted": true,
"message": "Customer data purged successfully"
}
}curl --location --request DELETE 'https://staging.api.liquidcommerce.cloud/users/purge/usrid_123abc456def' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>'Authorization: Bearer <YOUR_ACCESS_TOKEN>curl --location 'https://staging.api.liquidcommerce.cloud/users/fetch/usrid_123abc456def'
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>'{
"statusCode": 200,
"message": "Get the selected user",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731596453011,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/fetch/usrid_123abc456def",
"version": "1.7.0"
},
"data": {
"id": "usr_123abc456def",
"email": "[email protected]",
"firstName": "John",
"company": "",
"lastName": "Smith",
"phone": null,
"profileImage": "",
"birthDate": null,
"createdAt": "2024-11-14T13:17:47.482Z",
"updatedAt": "2024-11-14T14:50:54.613Z",
"addresses": [],
"savedPayments": []
}
}enum ENUM_CHECKOUT_STATUS_CODE_ERROR {
REQUEST_DEFAULT_ERROR = 5480,
REQUEST_LOCATION_OOS_ERROR = 5481,
REQUEST_LOCATION_MISMATCH_ERROR = 5482,
REQUEST_BIRTHDATE_ERROR = 5483,
REQUEST_CART_NOT_AVAILABLE_ERROR = 5484,
REQUEST_CART_ID_ERROR = 5485,
REQUEST_CART_ITEM_ERROR = 5486,
REQUEST_VALIDATION_ERROR = 5487,
REQUEST_TAX_ERROR = 5488,
REQUEST_COMPLETE_TOKEN = 5489,
REQUEST_DEFAULT_COMPLETE_ERROR = 5490,
REQUEST_CHECKOUT_COMPLETE_UPDATE_ERROR = 5491,
REQUEST_CHECKOUT_COMPLETE_SAVE_ERROR = 5492,
REQUEST_CHECKOUT_HAS_COMPLETE_ERROR = 5493,
REQUEST_NO_CART_ITEM_ERROR = 5494,
REQUEST_NO_CUSTOMER_FOUND_ERROR = 5495,
REQUEST_PAYMENT_ATTACHED_ERROR = 5496,
REQUEST_SHIPPING_ADDRESS_ERROR = 5497,
REQUEST_BILLING_ADDRESS_ERROR = 5498,
REQUEST_PAYMENT_NOT_FOUND_ERROR = 5499,
REQUEST_CART_UPDATED_ERROR = 5501,
REQUEST_ADDRESS_DEFAULT_ERROR = 5502,
REQUEST_TIPS_ERROR = 5503,
REQUEST_COMPLETE_CUSTOMER_MISSING_FIELDS = 5504,
REQUEST_RETAILER_HOURS_ERROR = 5505,
REQUEST_ITEM_QUANTITY_CHANGE_ERROR = 5506,
REQUEST_MAX_QUANTITY_PER_ORDER_ERROR = 5507,
REQUEST_PRESALE_NOT_STARTED_ERROR = 5508,
REQUEST_CART_MIN_RETAILER_NOT_MET_ERROR = 5509,
REQUEST_CHECKOUT_PROCESSING_LOCK_NOT_ACQUIRED_ERROR = 5510
}
enum ENUM_CHECKOUT_STATUS_CODE_MESSAGE {
REQUEST_DEFAULT_ERROR = "There's been an error with your checkout request.",
REQUEST_LOCATION_OOS_ERROR = 'The requested items are out of stock at this location.',
REQUEST_LOCATION_MISMATCH_ERROR = "The selected location doesn't match your cart items.",
REQUEST_BIRTHDATE_ERROR = 'Please verify your birthdate and try again.',
REQUEST_CART_NOT_AVAILABLE_ERROR = 'This cart is no longer available.',
REQUEST_CART_ID_ERROR = 'The cartId requested is invalid, check and try again.',
REQUEST_CART_ITEM_ERROR = "There's an issue with one or more items in your cart.",
REQUEST_VALIDATION_ERROR = "There's been an error with your request parameters, check and try again.",
REQUEST_TAX_ERROR = 'There was an error calculating tax for your order.',
REQUEST_COMPLETE_TOKEN = 'The checkout token provided is invalid, check and try again.',
REQUEST_DEFAULT_COMPLETE_ERROR = 'There was an error completing your checkout, confirm through the (prepare) method and try again.',
REQUEST_CHECKOUT_COMPLETE_UPDATE_ERROR = 'Unable to update your checkout status.',
REQUEST_CHECKOUT_COMPLETE_SAVE_ERROR = 'Unable to save your completed checkout.',
REQUEST_CHECKOUT_HAS_COMPLETE_ERROR = 'This checkout has already been processed, create a new cart to process a new checkout.',
REQUEST_NO_CART_ITEM_ERROR = 'Item(s) in your cart are no longer available.',
REQUEST_NO_CUSTOMER_FOUND_ERROR = 'The customer account was not found.',
REQUEST_PAYMENT_ATTACHED_ERROR = 'The payment attached to the checkout is not a valid payment method for this customer.',
REQUEST_SHIPPING_ADDRESS_ERROR = 'The address in your cart has changed, check and try again.',
REQUEST_BILLING_ADDRESS_ERROR = 'The billing address in your checkout is not valid, check and try again.',
REQUEST_PAYMENT_NOT_FOUND_ERROR = 'The payment method provided was not found.',
REQUEST_CART_UPDATED_ERROR = 'The cart requested was updated during your checkout.',
REQUEST_ADDRESS_DEFAULT_ERROR = "There's been an error with your address configurations in cart and/or billing address, check and try again.",
REQUEST_TIPS_ERROR = "There's been an error applying your tips to the checkout.",
REQUEST_COMPLETE_CUSTOMER_MISSING_FIELDS = 'Customer profile information is incomplete. Please provide all required details.',
REQUEST_RETAILER_HOURS_ERROR = 'The retailer is currently closed or on-demand hours are not available.',
REQUEST_ITEM_QUANTITY_CHANGE_ERROR = 'Some items in your cart exceed available stock quantities. Please adjust your cart and try again.',
REQUEST_MAX_QUANTITY_PER_ORDER_ERROR = 'You have exceeded the maximum quantity allowed per order for one or more items in your cart.',
REQUEST_PRESALE_NOT_STARTED_ERROR = 'The presale for this item has not started yet. Please check back later.',
REQUEST_CART_MIN_RETAILER_NOT_MET_ERROR = 'Some items in your cart do not meet the minimum retailer requirements per order quantity. Please adjust your cart and try again.',
REQUEST_CHECKOUT_PROCESSING_LOCK_NOT_ACQUIRED_ERROR = 'This checkout is currently being processed, please try again later.',
}5494: Empty cart error5501: Cart updated during checkout
5492: Unable to save checkout5493: Checkout already processed
5510: Checkout currently being processed
5499: Payment method not found5502: Address configuration error
OutOfStock: Requested items are not available in the desired quantity
ItemsNotAdded: Products could not be added to the cart
ItemsRequestedNotAdded: These item(s) requested can't be added to the cart either due to the location provided, and/or hours of operation.
RemovedExistingCartItems: The following cart item(s) have been removed, they're currently not available in the location provided
ItemQuantityChange: Item quantity reduced from ${originalQuantity} to ${stock} due to limited stock availability
ItemsRemoved: The following item(s) were removed from the cart due to zero quantity
RetailerMinNotMet: The retailer minimum was not met, for item(s) (quantity:${quantity}, total price: ${price}) min(${min}).
NoItemsInCart: Cart is empty
InvalidId: The cartId provided is invalid, a new cart was created
NoId: The cartId provided is empty, a new cart was created
PresaleLimitExceeded: Presale fully reserved || Only ${n} units remaining for presale
PresaleNotStarted: Presale starts at ${date}
PresaleExpired: Presale reservation has expired || This presale has ended
PresaleMixedCart: Presale items cannot be mixed with regular items
RetailerFulfillmentInvalid: The retailer fulfillment ${fulfillmentId} selected, for item(s) with quantity:${quantity}, is invalid due to the fulfillment's allowed product type ${productTypesAllowed}.
MaxQuantityPerOrderExceeded: The maximum quantity per order for this item is ${maxPresaleQuantity}. Quantity has been adjusted accordingly. || Quantity adjusted from ${originalQuantity} to ${quantity} due to presale availability
RetailerOnDemandHoursNotAvailable: The following cart item(s) have been removed due to retailer hours of operation restrictions
AddressChange: The provided location does not match the cart's location
CartCheckoutProcessed: The cartId (${id}) provided has already been processed through a checkout successfully
NewCart: No cart was found with the cartId (${id}), a new cart was created
CartError: Default error event
ItemIdNotFound: The following item(s) with provided id were not found in the cart
ItemEngravingError: The item(s) selected for engraving is not engravable. || The item(s) selected for engraving is not currently active for engraving. || The item(s) selected for engraving has exceeded the maximum(${maxLines}) number of engraving lines. || The item(s) selected for engraving has exceeded the maximum(${engravingPersonalization.maxCharsPerLine}) characters limit per line.
CouponProcessingError: There was an error processing this coupon
CouponNotFound: This coupon does not exist
CouponExpired: This coupon has expired
InvalidCoupon: This coupon is invalid
NoApplicableDiscount: This coupon results in no discount
CouponNotStarted: This coupon is not yet active
MinimumOrderValueNotMet: Order total does not meet minimum required value
MinimumOrderUnitsNotMet: Order does not meet minimum quantity requirement
MinimumDistinctItemsNotMet: Order does not meet minimum unique items requirement
QuotaExceeded: This coupon has reached its usage limit
UserLimitExceeded: Default error event
NotFirstPurchase: This coupon is only valid for first purchases
InvalidMembership: This coupon requires a specific membership
InvalidDomain: This coupon is restricted to specific email domains
InvalidRequirements: This coupon does not meet the required conditions
InvalidOrganization: This coupon is restricted to specific organizations
PresaleItemsNotAllowed: This coupon cannot be used with presale items
ProductNotEligible: One or more items are not eligible for this coupon
NotEnoughPreviousOrders: Previous order requirement not met
RetailerDoesNotAllowPromos: This retailer does not accept promo codes
RetailersDoNotAllowPromos: Selected retailers do not accept promo codes
Checkout Only Event Cases
RetailerDoesNotAllowGiftCards: Gift card applied. Note: ${retailerNames} does not accept gift cards || Gift card applied to eligible retailers only. The following retailers do not accept gift cards: ${retailerNames}
RetailersDoNotAllowGiftCards: ${retailerNames} does not accept gift cards || None of the selected retailers accept gift cards
interface ICartEvent {
type: CART_EVENT_ENUM;
message: string;
items?: Array<Partial<ICartItem>>;
}enum CART_EVENT_ENUM {
OOS = 'OutOfStock',
PRESALE_LIMIT_EXCEEDED = 'PresaleLimitExceeded',
PRESALE_NOT_STARTED = 'PresaleNotStarted',
PRESALE_EXPIRED = 'PresaleExpired',
PRESALE_MIXED_CART = 'PresaleMixedCart',
ITEMS_REQUESTED_NOT_ADDED = 'ItemsRequestedNotAdded',
ITEM_NOT_ENGRAVED = 'ItemEngravingError',
ADDRESS_CHANGE = 'AddressChange',
REMOVED_EXISTING_ITEMS = 'RemovedExistingCartItems',
RETAILER_MIN = 'RetailerMinNotMet',
NO_ITEMS_IN_CART = 'NoItemsInCart',
INVALID_ID = 'InvalidId',
NO_ID = 'NoId',
CART_CHECKOUT_PROCESSED = 'CartCheckoutProcessed',
NEW_CART = 'NewCart',
DEFAULT = 'CartError',
ITEM_QTY_CHANGE = 'ItemQuantityChange',
ITEM_ID_NOT_FOUND = 'ItemIdNotFound',
ITEMS_REMOVED = 'ItemsRemoved',
RETAILER_FULFILLMENT_INVALID = 'RetailerFulfillmentInvalid',
MAX_QUANTITY_PER_ORDER_EXCEEDED = 'MaxQuantityPerOrderExceeded',
RETAILER_ONDEMAND_HOURS_NOT_AVAILABLE = 'RetailerOnDemandHoursNotAvailable',
COUPON_PROCESSING_ERROR = 'CouponProcessingError',
COUPON_NOT_FOUND = 'CouponNotFound',
COUPON_EXPIRED = 'CouponExpired',
NO_APPLICABLE_DISCOUNT = 'NoApplicableDiscount',
COUPON_NOT_STARTED = 'CouponNotStarted',
MINIMUM_ORDER_VALUE_NOT_MET = 'MinimumOrderValueNotMet',
MINIMUM_ORDER_UNITS_NOT_MET = 'MinimumOrderUnitsNotMet',
MINIMUM_DISTINCT_ITEMS_NOT_MET = 'MinimumDistinctItemsNotMet',
QUOTA_EXCEEDED = 'QuotaExceeded',
NOT_FIRST_PURCHASE = 'NotFirstPurchase',
INVALID_COUPON = 'InvalidCoupon',
INVALID_MEMBERSHIP = 'InvalidMembership',
INVALID_DOMAIN = 'InvalidDomain',
INVALID_REQUIREMENTS = 'InvalidRequirements',
INVALID_ORGANIZATION = 'InvalidOrganization',
PRESALE_ITEMS_NOT_ALLOWED = 'PresaleItemsNotAllowed',
PRODUCT_NOT_ELIGIBLE = 'ProductNotEligible',
NOT_ENOUGH_PREVIOUS_ORDERS = 'NotEnoughPreviousOrders',
RETAILER_DOES_NOT_ALLOW_PROMOS = 'RetailerDoesNotAllowPromos',
RETAILERS_DO_NOT_ALLOW_PROMOS = 'RetailersDoNotAllowPromos',
RETAILER_DOES_NOT_ALLOW_GIFT_CARDS = 'RetailerDoesNotAllowGiftCards',
RETAILERS_DO_NOT_ALLOW_GIFT_CARDS = 'RetailersDoNotAllowGiftCards',
}data.message
string
Delete message
userId
string
Existing user identifier, returned upon session creation or update
paymentId
string
Existing payment identifier of the user
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
data.deleted
boolean
States if delete succeded or not
data
User payment details
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
userId
string
Identifier of the user to add payment method for
paymentMethodId
string
Payment method ID or new payment token, you must use the CLOUD SDK to mount/inject a paymentElement to be able to generate a payment token.
isDefault
boolean
Set as default payment method (default: false)
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
data.message
string
Delete message
addressId
string
Existing user address identifier, returned upon address creation or update
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
data.deleted
boolean
States if delete succeded or not
The LiquidCommerce Cloud SDK uses API keys to authenticate requests. You can request your keys from your Partnerships liaison.
Your API keys carry many privileges, so be sure to keep them secure! Do not share your secret API keys in publicly accessible areas such as GitHub, client-side code, and so forth.
All API requests in production must be made over HTTPS. Calls made over plain HTTP will fail. API requests without authentication will also fail.
We've included a comprehensive demo application to help you quickly understand how to implement the LiquidCommerce Cloud SDK in real-world scenarios.
Clone the repository:
Navigate to the demo directory
Add your API keys to the demo:
Open the demo directory in a code editor
Locate and edit the index.html file
Replace the placeholder values with your actual API keys:
Save the file
⚠️ Note: Keep your API keys secure and never commit them to public repositories.
Start the development server by either:
Running pnpm run dev and opening http://localhost:3000/
Opening the file directly in your browser
This browser-based demo demonstrates client-side implementation of the LiquidCommerce Cloud SDK including:
SDK initialization in the browser
API authentication flow
Address validation
Catalog browsing and product search
Shopping cart integration
User account management and session handling
Payment integration
Interactive checkout process
Orders
Authentication
Webhook Testing
Fetch By ID
The demo uses vanilla JavaScript to ensure compatibility and clear understanding of SDK implementation without framework-specific code. 👉 View Demo Source Code
order.number
string
Order number
order.referenceId
string
Order reference id
data.message
string
Delete message
Below find the loc object reference for Liquid Commerce's location based configurations.
The loc parameter provides 2 different options for location based information through the following parameters, at least 1 is required: *longitude and latitude are prioritized if included in the request*
Address: location information split out to individual values for the street, city, state, zip code and country. If you choose to use this option for your location, only the state is required.
*ALL FIELDS REQUIRED FOR ON DEMAND AVAILABILITY*
Longitude & Latitude: location information provided through an exact geospatial point by providing valid longitude and latitude coordinates values.
locThe LiquidCommerce Checkout API provides a secure, PCI-compliant transaction processing system optimized for beverage alcohol e-commerce
This API handles the final steps of the purchasing process, including payment processing, order creation, and confirmation.
Secure payment processing
Order creation and management
Support for multiple payment methods
Integration with cart and inventory systems
The Checkout API is designed with security as a top priority:
PCI DSS Compliance: Fully compliant with Payment Card Industry Data Security Standard, ensuring secure handling of payment information.
Tokenization: Sensitive payment data is tokenized to minimize risk.
Encryption: All data is encrypted in transit and at rest.
Fraud Detection: Implements advanced fraud detection mechanisms.
Before using the API, you need to obtain an access token from the . Include this token in the Authorization header of all API requests:
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain a JSON object with more details about the error.
Checkout complete uses strict error handling with specific status codes.
The API is subject to rate limiting to ensure fair usage and maintain performance. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response.
curl --location 'https://staging.api.liquidcommerce.cloud/users/payments/purge' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "usrid_123abc456def",
"paymentMethodId": "paymentid_123abc456def",
"isDefault": true
}'{
"statusCode": 200,
"message": "Deleting a payment method",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731600946784,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/payments/purge/usr_123abc456def/paymentid_123abc456def",
"version": "1.7.0"
},
"data": {
"deleted": false,
"message": "There's no Payment method with the ID provided for this customer"
}
}curl --location 'https://staging.api.liquidcommerce.cloud/users/payments/add' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"customerId": "usrid_123abc456def",
"paymentMethodId": "paymentid_123abc456def",
"isDefault": true
}'{
"statusCode": 201,
"message": "Adding an new payment method succeeded",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731625068720,
"timezone": "UTC",
"requestId": "requestid_123abc456def",
"path": "/api/users/payments/add",
"version": "1.7.0"
},
"data": {
"id": "paymentid_123abc456def",
"type": "card",
"isDefault": true,
"card": {
"brand": "visa",
"country": "US",
"expMonth": 11,
"expYear": 2031,
"last4": "1111",
"funding": "credit"
},
"createdAt": "2024-11-14T22:57:49.094Z"
}
}curl --location --request DELETE 'https://staging.api.liquidcommerce.cloud/addresses/purge/addrid_123abc45def' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>'{
"statusCode": 200,
"message": "Deleting an address",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731599013239,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/addresses/purge/addrid_123abc45def",
"version": "1.7.0"
},
"data": {
"deleted": true,
"message": "Address data purged successfully"
}
}const API_KEY = 'YOUR_LIQUIDCOMMERCE_API_KEY';
const GOOGLE_PLACES_API_KEY = 'PUT_YOUR_KEY_HERE';
// For Orders
const ORDER_API_USER = 'YOUR_ORDER_API_USER';
const ORDER_API_PASSWORD = 'YOUR_ORDER_API_PASSWORD';npm install @liquidcommerce/cloud-sdk
# or
yarn add @liquidcommerce/cloud-sdkgit clone https://github.com/liquidcommerce/cloud-sdk.gitcd cloud-sdk/demozip code
country: string [optional]
country in which the address is located
address: Address
location information split out to individual values for the street, city, state, zip code and country. When using this option, all fields except country are required.
coords: Coords
location information provided through an exact geospatial point by providing valid longitude and latitude coordinates values. The numbers are in decimal degrees format and range from -90 to 90 for latitude and -180 to 180 for longitude
id: string [optional]
identifier of the address
one: string [required]
street address
two: string [required]
apt, suite, floor, etc
city: string [required]
city
state: string [required]
state, you can use either 2 letter code or the full name
lat: number [required]
latitude, ex: 40.744860
long: number [required]
longitude, ex: -73.985314
zip: string [required]
Real-time inventory checks
Support for promotional codes and discounts
Our dedicated support team is available to assist you with API integration and usage. For any questions or issues, please contact [email protected].
"address": {
// street address
// REQUIRED
"one": "string";
// apt, suite, floor, etc
// REQUIRED
"two": "string";
// city
// REQUIRED
"city": "string";
// state, you can use either 2 letter code or the full name
// REQUIRED
"state": "string";
// zip code
// REQUIRED
"zip": "string";
// country
// OPTIONAL
"country": "string";
}// The numbers are in decimal degrees format and range
// from -90 to 90 for latitude and -180 to 180 for longitude
"coords": {
// latitude
// REQUIRED
"lat": "number", // ex: 40.744860
// longitude
// REQUIRED
"long": "number" // ex: -73.985314
}{
"address":{
"one": "100 Madison ave",
"two": "apt 1707",
"city": "New York",
"state": "NY",
"zip": "10016",
"country": "US"
},
"coords": {
"lat": 40.744860,
"long": -73.985314
}
}Authorization: Bearer <YOUR_ACCESS_TOKEN>city
string
City name
state
string
Two-letter state code or full state name
zip
string
Postal/ZIP code
type
string
Address type ('shipping' or 'billing')
placesId
string
Google Places API location identifier
lat
number
Latitude coordinate of address
long
number
Longitude coordinate of address
isDefault
boolean
Set as default address for type, default: false
data
Address object added
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
userId
string
Identifier of the user to add address for
one
string
Primary street address
two
string
Secondary address (apt, suite, etc.)
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
curl --location 'https://staging.api.liquidcommerce.cloud/users/addresses/add' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"userId": "usrid_123abc456def",
"one": "100 Madison Ave",
"two": "Apt 1707",
"city": "New York",
"state": "NY",
"zip": "10016",
"type": "shipping",
"placesId": "placeid_123abc456ef",
"lat": 40.7447986,
"long": -73.98530819999999,
"isDefault": true
}'{
"statusCode": 201,
"message": "Updating or creating an new address succeeded",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731598717444,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/addresses/add",
"version": "1.7.0"
},
"data": {
"id": "addrid_123abc45def",
"type": "shipping",
"one": "100 Madison Ave",
"two": "Apt 1707",
"city": "New York",
"state": "NY",
"zip": "10016",
"lat": 40.7447986,
"long": -73.98530819999999,
"placesId": "placeid_123abc456ef",
"country": "US",
"createdAt": "2024-11-14T15:38:37.775Z",
"updatedAt": "2024-11-14T15:38:37.775Z",
"isDefault": true
}
}path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
firstName: string
user's first name
lastName: string
user's last name
phone: string
user's phone number
company: string
user's company
profileImage: string
url to user's profile image
birthDate: string
users's birth date
createdAt: Date
account creation timestamp
updatedAt: Date
last update timestamp
addresses: Array<>
user's saved addresses
savedPayments: Array<>
user's saved payment methods
id: string
user address identifier
placesId: string
google places identifier
one: string
the primary street address or neighborhood
two: string
the secondary address information, such as apt or suit number
city: string
the name of the city
id: string
user payment identifier
type: string
payment method type (e.g., credit card, paypal)
isDefault: boolean
whether this is the default payment method
card: [optional]
card details if payment method is a card
createdAt: Date
payment method creation timestamp
brand: string
card brand (Visa, Mastercard, etc.)
country: string
card issuing country
expMonth: number
expiration month
expYear: number
expiration year
last4: string
last 4 digits of card number
id: string
user's identifier
email: string
user's email address
{
"id": "userId_123abc12_123abc12",
"email": "[email protected]",
"firstName": "John",
"company": "Liquid Commerce",
"lastName": "Doe",
"phone": "+1234567890",
"profileImage": "https://example.com/profiles/sarah.jpg",
"birthDate": "1990-05-15",
"createdAt": "2024-09-15T10:30:25.123Z",
"updatedAt": "2024-11-12T16:45:32.891Z",
"addresses": [
{
"id": "8d4e5f6g-7h8i-9j0k-lm12-34567890nopq",
"type": "shipping",
"one": "100 Madison Ave",
"two": "Floor 23",
"city": "New York",
"state": "NY",
"zip": "10166",
"country": "US",
"placesId": "ChIJKxDbe7lZwokRVf__________",
"lat": 40.754542,
"long": -73.976929,
"isDefault": true,
"createdAt": "2024-10-01T09:22:15.432Z",
"updatedAt": "2024-11-10T11:15:44.789Z"
}
],
"savedPayments": [
{
"id": "pm_paymentId_123abc12",
"type": "card",
"isDefault": true,
"card": {
"brand": "visa",
"country": "US",
"expMonth": 12,
"expYear": 20**,
"last4": "****",
"funding": "credit"
},
"createdAt": "2024-10-15T14:30:20.555Z"
}
],
"session": {
"key": "pk_test_51AbCdEfGhIjKl_________",
"secret": "seti_1AbCdEfGhIjKlM_________",
"createdAt": "2024-11-12T16:45:32.891Z"
}
}state: string
the name of the state or region
zip: string
the postal code
country: Date
the name of the country
lat: number
the latitude coordinate of the address
long: number
the longitude coordinate of the address
createdAt: Date
account creation timestamp
updatedAt: Date
last update timestamp
isDefault: boolean
whether this is the default address
funding: string
funding type (credit, debit, etc.)
A full reference to Liquid Commerce's Catalog API filters.
All the available options and parameters for filters when using the Catalog API to get catalog product data.
key: string
Below are all the available filter key values that are available:
brands: Array<string>
flavor: Array<string>
region: Array<string
There are 3 different types allowed as acceptable values for the filters, object, array, string[]. Below are example of different type variation.
categories: Array<string>
price: object
availability: string
All the available filter properties received from catalog response, to be used in filtering results further on consequent catalog requests.
catalog filtersvariety: Array<string>
fulfillment: Array<Modalities>
presale: Binary
tags: Array<string>
price: object
availability: Availability
categories: Array<Taxonomy>
sizes: Array<string>
colors: Array<string>
appellation: Array<string>
country: Array<string>
vintage: Array<string>
materials: Array<string>
collectionTags: Array<string>
The filter key to be applied (e.g., "tags", "price")
values: string[] | object | string
The values to filter by, type depends on the filter key
AVAILABILITY_UNSPECIFIED: string
Default value when availability is not specified
IN_STOCK: string
Products currently in stock
OUT_OF_STOCK: string
Products currently out of stock
PREORDER: string
Products available for pre-order
BACKORDER: string
Products available for back-order
YES: string
Products that can be engraved
NO: string
Products that cannot be engraved
YES: string
Represents a positive condition
NO: string
Represents a negative condition
shipping: string
Products available for shipping
onDemand: string
Products on demand
bucket: string(Available Filters)
filter key, ex: brands, availability, category
values: Array<FilterSchemaValue>
values, has 2 properties, value & count, ex:
value: string
filter option
count: number
matching items count
{
"key": "categories",
"values": [
"SPIRITS > VODKA"
]
}{
"key": "price",
// REQUIRED: at least, min or max
// must be provided
"values": {
// OPTIONAL: if omitted, accounted for as
// negative to infinity
"min": 5,
// OPTIONAL: if omitted, accounted for as
// positive to infinity
"max": 29
}
}{
"key": "availability",
// Available values: IN_STOCK | OUT_OF_STOCK
"values": "IN_STOCK"
}{
"key": "engraving",
"values": "YES"
}jsonCopy{
"key": "fulfillment",
"values": ["onDemand"]
}[
{
"bucket": "brands",
"values": [
{
"value": "TITO'S",
"count": 1
}
]
},
{
"bucket": "categories",
"values": [
{
"value": "SPIRITS",
"count": 1
},
{
"value": "SPIRITS > VODKA",
"count": 1
}
]
},
]{
"filters": [
{
"key": "categories",
"values": [
"SPIRITS > VODKA"
]
},
{
"key": "price",
"values": {
"min": 5,
"max": 38
}
},
{
"key": "availability",
"values": "IN_STOCK"
},
{
"key": "collectionTags",
"values": [
"RESERVEBAR PICKS"
]
}
],
}total matching items
availableOrderBy: Array<string>
available sort fields
availableOrderDirections: Array<string>
sort directions
cursor:
pagination cursors
filters:
available filters
matched products
navigation: NavigationSchema
navigation and filtering info
id: string
navigation session id
correctedQuery: string
spell-corrected search query
attributionToken: string
analytics tracking token
currentPage: number
current page number
totalPages: number
total available pages
nextPageToken: string
token for next page
previousPageToken: string
token for previous page
totalCount: number
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
id
string
Cart ID (leave empty to create a new cart)
items
array<>
Array of items to add to the cart
loc
Location object for determining availability and shipping
id
string
This parameter is REQUIRED when:
Updating any personalized configurations for a specific item in the cart (e.g., engraving messages, or other personalization options)
Deleting a product that has personalizations
partNumber
string
Unique identifier for the retailer's product variant
quantity
number
Quantity of the item
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
identifier
string
Order ID, returned upon checkout complete
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
data
Order object
The LiquidCommerce SDK provides comprehensive support for presale products, allowing merchants to offer products for purchase before general availability. This guide covers how to integrate presale functionality into your application.
Presales enable customers to reserve and purchase products before they become widely available. Key features include:
Advanced Purchase: Customers can buy products before stock arrival
Limited Quantities: Presale products often have purchase limits
Scheduled Availability: Products have defined presale windows
Exclusive Access: First-come, first-served inventory reservation
Use the catalog search to find presale products:
When working with presale items, the cart response includes additional fields:
The SDK provides specific event types for presale scenarios:
Example event handling:
Complete the checkout process promptly when isPresaleLocked is true:
Monitor the reservation time when dealing with presales:
Cart Exclusivity: Presale items require dedicated carts
Time Constraints: Reservations have expiration times
Quantity Restrictions: Limited inventory per customer
Geographic Availability: Some presales may be region-specific
Transparency: Always show presale status and estimated shipping
Urgency: Display reservation timers when applicable
Clarity: Explain why presale items need separate orders
Feedback: Provide clear error messages for presale-specific issues
The presale feature in LiquidCommerce SDK enables:
Early access to limited products
Automated inventory management
Time-based reservations
Clear event-driven feedback
Successful presale integration requires careful attention to:
Event handling for various presale states
Time-sensitive checkout flows
Clear user communication
Proper cart segregation
By following this guide and the provided examples, you can create a smooth presale experience that maximizes conversion while maintaining inventory integrity.
"products": [
{
"id": "ful_def456uvw123",
"salsifyGrouping": "GRP-78392",
"name": "Maestro Dobel® Diamante Tequila Cristalino",
"brand": "Maestro Dobel",
"catPath": "Spirits > Tequila > Cristalino",
"category": "",
"classification": "",
"type": "",
"subType": "",
"region": "Jalisco",
"country": "Mexico",
"material": "Agave",
"color": "White",
"flavor": "",
"variety": "100% Blue Agave",
"appellation": "",
"abv": "40",
"proof": "80",
"age": "",
"vintage": "",
"description": "Maestro Dobel® Diamante® is the original Cristalino. Blended extra Añejo, Añejo and reposado tequilas are aged in Balkan new white wood barrels, then filtered again to retain an elegant flavor. The Cristalino tequila has a mild oak aroma with a touch of vanilla. It has a citrus and prickly pear flavor with a smooth, crisp, clean finish. Pour one shot of straight Diamante Tequila to sip, into an old-fashioned glass with ice and a lemon slice, or as a Margarita. (80 proof)",
"htmlDescription": "<p>Maestro Dobel® Diamante® is the original Cristalino. Blended extra Añejo, Añejo and reposado tequilas are aged in Balkan new white wood barrels, then filtered again to retain an elegant flavor. The Cristalino tequila has a mild oak aroma with a touch of vanilla. It has a citrus and prickly pear flavor with a smooth, crisp, clean finish. Pour one shot of straight Diamante Tequila to sip, into an old-fashioned glass with ice and a lemon slice, or as a Margarita. (80 proof)</p>",
"tastingNotes": "World's first multi-aged clear tequila.",
"images": [
"https://example.com/products/dobel-diamante-1.jpg",
"https://example.com/products/dobel-diamante-2.jpg"
],
"sizes": [
{
"id": "sz_xy789zw456",
"upc": "00811538012034",
"size": "750 ML",
"volume": "750",
"uom": "MILLILITRE",
"container": "Bottle",
"containerType": "Bottle",
"pack": false,
"packDesc": "",
"image": "https://example.com/products/dobel-diamante-bottle.jpg",
"attributes": {
"engraving": {
"status": true,
"maxLines": 3,
"maxCharsPerLine": 16,
"fee": 5000,
"location": "Back of the Bottle"
}
}
}
]
},
{
"id": "ful_ghi789rst123",
"salsifyGrouping": "649066c19661fb45f6869934",
"name": "The Macallan Double Cask 12 Years Old Single Malt Whisky",
"brand": "The Macallan",
"catPath": "Spirits > Whiskey > Scotch",
"category": "",
"classification": "",
"type": "",
"subType": "",
"region": "Speyside",
"country": "Scotland",
"material": "Grain",
"color": "Tawny/brown",
"flavor": "",
"variety": "Barley",
"appellation": "Speyside",
"abv": "40",
"proof": "80",
"age": "",
"vintage": "",
"description": "The Macallan Double Cask 12-Year-Old pairs the indulgent fruit, caramel, and oak spice character of Sherry-seasoned European oak with the bright citrus and vanilla notes of Sherry-seasoned American oak for a satisfyingly rich and perfectly balanced flavor experience. Awarded unanimous Double Gold upon release.",
"htmlDescription": "<p>The Macallan Double Cask 12-Year-Old pairs the indulgent fruit, caramel, and oak spice character of Sherry-seasoned European oak with the bright citrus and vanilla notes of Sherry-seasoned American oak for a satisfyingly rich and perfectly balanced flavor experience. Awarded unanimous Double Gold upon release.</p>",
"tastingNotes": "\"If there is royalty in the whisky world, it belongs to Scotland, and if there is a king of Scotch whisky, it's The Macallan.\" – Forbes",
"images": [
"https://example.com/products/macallan-12-1.jpg"
],
"sizes": [
{
"id": "sz_pqr123mn456",
"upc": "00812066021598",
"size": "750 ML",
"volume": "750",
"uom": "MILLILITRE",
"container": "Bottle",
"containerType": "Bottle",
"pack": false,
"packDesc": "",
"image": "https://example.com/products/macallan-12-bottle.jpg",
"attributes": {
"engraving": {
"status": true,
"maxLines": 3,
"maxCharsPerLine": 16,
"fee": 5000,
"location": "Below the Label"
}
}
}
]
}
],
"retailers": [
{
"id": "ret_abc123xyz789",
"name": "LiquidCommerce Wine & Spirits",
"platformFee": 499,
"address": {
"one": "240 Loisaida Avenue",
"two": "",
"city": "New York",
"state": "NY",
"zip": "10009",
"country": "US"
},
"fulfillments": [
{
"id": "ful_ghi189rst123",
"timezone": "America/New_York",
"type": "onDemand",
"canEngrave": false,
"fees": {
"min": 1999,
"fee": 0,
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Arrives in 60 mins",
"short": "60 mins"
},
"hours": {
"monday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"tuesday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"wednesday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"thursday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"friday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"saturday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"sunday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
}
},
"breaks": [],
"items": []
}
]
},
{
"id": "ret_abc123xyz189",
"name": "LiquidCommerce Barn",
"platformFee": 499,
"address": {
"one": "1000 N 5th Ave",
"two": "134",
"city": "Vernon Hills",
"state": "NY",
"zip": "10061",
"country": "US"
},
"fulfillments": [
{
"id": "ful_abc123xyz189",
"timezone": "America/Chicago",
"type": "shipping",
"canEngrave": true,
"fees": {
"pack": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"individual": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Ships in 2-3 days",
"short": "2-3 days"
},
"hours": {
"monday": {
"active": false,
"times": []
},
"tuesday": {
"active": false,
"times": []
},
"wednesday": {
"active": false,
"times": []
},
"thursday": {
"active": false,
"times": []
},
"friday": {
"active": false,
"times": []
},
"saturday": {
"active": false,
"times": []
},
"sunday": {
"active": false,
"times": []
}
},
"breaks": [],
"items": []
}
]
}
]{
"statusCode": 200,
"message": "Create, build and manage carts.",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731610845174,
"timezone": "UTC",
"requestId": "req_abc123xyz789",
"path": "/api/cart/update",
"version": "1.7.0"
},
"cart": {
"id": "cart_abc123xyz789",
"quantity": 1,
"platformFee": 499,
"deliveryFee": 0,
"shippingFee": 1599,
"engravingFee": 0,
"discounts": 0,
"giftCardTotal": 0,
"subtotal": 5999,
"total": 8097,
"createdAt": "2024-11-14T19:00:45.433Z",
"updatedAt": "2024-11-14T19:00:45.433Z",
"items": [
{
"id": "item_abc123xyz789",
"variantId": "var_abc123xyz789",
"liquidId": "liq_abc123xyz789",
"retailerId": "ret_abc123xyz789",
"partNumber": "pn_abc123xyz789",
"fulfillmentId": "ful_abc123xyz789",
"upc": "88320002003",
"catPath": "WINE > ROSE WINE",
"volume": "1.5",
"uom": "LITRE",
"pack": false,
"packDesc": "",
"container": "Bottle",
"containerType": "Bottle",
"name": "Whispering Angel Rosé",
"brand": "Chateau D'esclans",
"scheduledFor": "",
"abv": "14",
"proof": "28",
"size": "1.5 L",
"price": 5999,
"quantity": 1,
"customerPlacement": "standard",
"maxQuantity": 12,
"unitPrice": 5999,
"availableAt": "",
"mainImage": "https://storage.example.com/images/products/image1.png",
"images": [
"https://storage.example.com/images/products/image1.png",
"https://storage.example.com/images/products/image2.png",
"https://storage.example.com/images/products/image3.png",
"https://storage.example.com/images/products/image4.png",
"https://storage.example.com/images/products/image5.png"
],
"attributes": {
"giftCard": {
"sender": "",
"message": "",
"recipients": [],
"sendDate": ""
},
"engraving": {
"isEngravable": true,
"hasEngraving": false,
"maxCharsPerLine": 0,
"maxLines": 0,
"fee": 0,
"location": "",
"lines": []
}
}
}
],
"loc": {
"coords": {
"lat": 40.7447986,
"long": -73.98530819999999
},
"address": {
"one": "100 madison ave",
"two": "apt 1707",
"city": "New york",
"state": "NY",
"zip": "10016",
"country": "US",
"placesId": "place_abc123xyz789",
"customerId": "cust_abc123xyz789",
"type": "shipping",
"lat": 40.7447986,
"long": -73.98530819999999
}
},
"retailers": [
{
"id": "ret_abc123xyz789",
"name": "East Houston St Wine & Liquors",
"platformFee": 499,
"address": {
"one": "250 E Houston Street",
"two": "",
"city": "New York",
"state": "NY",
"zip": "10002",
"country": "US"
},
"fulfillments": [
{
"id": "ful_abc123xyz789",
"timezone": "America/New_York",
"type": "shipping",
"canEngrave": false,
"fees": {
"pack": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"individual": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Ships in 2-3 days",
"short": "2-3 days"
},
"hours": {
"monday": {
"active": false,
"times": []
},
"tuesday": {
"active": false,
"times": []
},
"wednesday": {
"active": false,
"times": []
},
"thursday": {
"active": false,
"times": []
},
"friday": {
"active": false,
"times": []
},
"saturday": {
"active": false,
"times": []
},
"sunday": {
"active": false,
"times": []
}
},
"breaks": [],
"items": [
"item_abc123xyz789"
],
"engravingFee": 0,
"shippingFee": 1599,
"deliveryFee": 0,
"subtotal": 5999
}
],
"engravingFee": 0,
"deliveryFee": 0,
"shippingFee": 1599,
"subtotal": 5999,
"total": 8097
}
],
"attributes": {
"promoCode": {
"value": "",
"freeDelivery": false,
"freeServiceFee": false,
"freeShipping": false
},
"amounts": {
"fees": {
"shipping": 5999,
"onDemand": 0
},
"discounts": {
"shipping": 0,
"onDemand": 0,
"engraving": 0,
"service": 0,
"products": 0
}
}
},
"events": [
{
"type": "NoId",
"message": "The cartId provided is empty, a new cart was created",
"items": []
},
{
"type": "PartnerProductConfigs",
"message": "Item(s) have been removed, they're currently not available in your account's catalog. Check your LiquidCommerce Partner App account and add the product to you account, or just use our default catalog",
"items": [
{
"partNumber": "pn_abc123xyz789",
"quantity": 1,
"fulfillmentId": "ful_abc123xyz789",
"id": "item_abc123xyz789"
}
]
}
]
}
}curl --location 'https://staging.api.liquidcommerce.cloud/cart/update' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"id": "",
"items": [
{
"partNumber": "partnum_123abc456def",
"quantity": 1,
"fulfillmentId": "65830af0be8824843febdb8f"
},
{
"partNumber": "partnum_124abc456def",
"quantity": 1,
"engravingLines": [
"Happy birthday"
],
"fulfillmentId": "65830af0be8824843febdb72"
}
],
"loc": {
"address": {
"one": "100 madison ave",
"two": "apt 1707",
"city": "New york",
"state": "NY",
"zip": "10016"
}
},
"refresh": false
}'curl --location 'https://staging.api.liquidcommerce.cloud/orders/order_number'
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>'{
"statusCode": 200,
"message": "Order fetched successfully.",
"metadata": {
"languages": [
"en"
],
"timestamp": 1745257267418,
"timezone": "UTC",
"requestId": "1deb1965-279b-4458-9b39-1baf83ca4fe7",
"path": "/orders/1745257243084",
"version": "1.7.0"
},
"data": {
"referenceId": "RESV_90874794283742",
"legacyOrderNumber": "174423423423423",
"partnerId": "65ba4c2ea7f3d123456789ab",
"createdAt": "2025-04-21T17:40:43.000Z",
"updatedAt": "2025-04-21T17:40:49.000Z",
"isHybrid": true,
"customer": {
"id": 12643699,
"firstName": "Test",
"lastName": "Test",
"email": "[email protected]",
"phone": "(432) 424-2424",
"birthdate": "2004-04-21"
},
"addresses": {
"shipping": {
"one": "100 Madison Avenue",
"two": null,
"city": "Morristown",
"state": "NJ",
"zip": "07960",
"country": "US",
"firstName": "Test",
"lastName": "Test",
"email": "[email protected]",
"phone": "(432) 424-2424",
"company": null
},
"billing": {
"one": "100 Madison Avenue",
"two": null,
"city": "Morristown",
"state": "NJ",
"zip": "07960",
"country": "US",
"firstName": "Test",
"lastName": "Test",
"email": "[email protected]",
"phone": "(432) 424-2424",
"company": null
}
},
"options": {
"isGift": false,
"giftMessage": null,
"giftRecipient": {
"name": null,
"email": null,
"phone": null
},
"hasVerifiedAge": false,
"allowsSubstitution": true,
"billingSameAsShipping": true,
"deliveryInstructions": null,
"marketingPreferences": {
"email": true,
"sms": true
}
},
"amounts": {
"subtotal": 8638,
"shipping": 1998,
"platform": 1198,
"tax": 704,
"engraving": 0,
"service": 0,
"delivery": 0,
"discounts": 0,
"giftCards": 0,
"tip": 0,
"total": 12538,
"taxDetails": {
"products": 572,
"shipping": 132,
"delivery": 0,
"bag": 0,
"bottleDeposits": 0,
"retailDelivery": 0
},
"discountDetails": {
"products": 0,
"shipping": 0,
"delivery": 0,
"engraving": 0,
"service": 0
}
},
"paymentMethods": [
{
"type": "CREDIT_CARD",
"card": "Visa",
"last4": "1111",
"holder": "Test Test",
"code": null
}
],
"retailers": [
{
"id": "65ba4d9f1234567890abcdef",
"legacyId": "1798987",
"name": "Test Liquor & Wine Cellar",
"system": "ReserveBar OMS",
"timezone": "America/Chicago",
"address": {
"one": "6506 Liquor Test",
"two": "Suite 800",
"city": "Sugar Land",
"state": "NY",
"zip": "10019",
"country": "US",
"coordinates": {
"latitude": 29.6079351,
"longitude": -95.6592685
}
},
"fulfillments": [
{
"id": "65ba4db8abcdef1234567890",
"type": "shipping",
"status": "processing",
"scheduledFor": null,
"updatedAt": "2025-04-21T17:40:49.000Z",
"itemIds": [
"f47ac10b-58cc-4372-a567-0e02b2c3d479"
],
"packages": [],
"timeline": [
{
"status": "processing",
"timestamp": "2025-04-21T17:40:49.91184"
},
{
"status": "created",
"timestamp": "2025-04-21T17:40:45.711992"
}
]
}
],
"amounts": {
"subtotal": 4139,
"shipping": 999,
"platform": 0,
"tax": 340,
"engraving": 0,
"service": 0,
"delivery": 0,
"discounts": 0,
"giftCards": 0,
"tip": 0,
"total": 5478,
"taxDetails": {
"products": 274,
"shipping": 66,
"delivery": 0,
"bag": 0,
"bottleDeposits": 0,
"retailDelivery": 0
},
"discountDetails": {
"products": 0,
"shipping": 0,
"delivery": 0,
"engraving": 0,
"service": 0
}
}
},
{
"id": "65ba4df09876543210fedcba",
"legacyId": "89928",
"name": "Liquid's Elixir & Spirits",
"system": "ReserveBar OMS",
"timezone": "America/New_York",
"address": {
"one": "2627 Main Ave NW",
"two": null,
"city": "Dalton",
"state": "NC",
"zip": "20008",
"country": "US",
"coordinates": {
"latitude": 38.924376,
"longitude": -77.0515667
}
},
"fulfillments": [
{
"id": "65ba4e0812345678abcdef90",
"type": "shipping",
"status": "created",
"scheduledFor": null,
"updatedAt": "2025-04-21T17:40:45.000Z",
"itemIds": [
"d0782bd8-86c8-4053-93b5-a48a28f5648b"
],
"packages": [],
"timeline": [
{
"status": "created",
"timestamp": "2025-04-21T17:40:45.749774"
}
]
}
],
"amounts": {
"subtotal": 4499,
"shipping": 999,
"platform": 0,
"tax": 364,
"engraving": 0,
"service": 0,
"delivery": 0,
"discounts": 0,
"giftCards": 0,
"tip": 0,
"total": 5862,
"taxDetails": {
"products": 298,
"shipping": 66,
"delivery": 0,
"bag": 0,
"bottleDeposits": 0,
"retailDelivery": 0
},
"discountDetails": {
"products": 0,
"shipping": 0,
"delivery": 0,
"engraving": 0,
"service": 0
}
}
}
],
"items": [
{
"id": "d0782bd8-86c8-4053-93b5-a48a28f5648b",
"fulfillmentId": "65ba4f2312345678abcdef90",
"retailerId": "65ba4f359876543210fedcba",
"variantId": "65ba4f5712345678fedcba09",
"liquidId": "65ba4f40abcdef1234567890",
"legacyGrouping": null,
"legacyPid": "pid-abc",
"customerPlacement": "standard",
"isPresale": false,
"estimatedShipBy": null,
"product": {
"name": "Liquid Elixir",
"brand": "Liquid",
"upc": "001928391838291",
"sku": "958050",
"mskus": [
"LIQUID-ELIXIR"
],
"category": "SPIRITS > MEZCAL",
"size": "700 ML",
"volume": "700",
"uom": "ML",
"proof": null,
"attributes": {
"pack": false,
"packDescription": null,
"abv": "43",
"container": "BOTTLE",
"containerType": "Bottle"
}
},
"image": "https://assets.liquidcommerce.co/core/white_matte_bottle_nobg.png",
"pricing": {
"price": 4139,
"unitPrice": 4139,
"quantity": 1,
"tax": 274,
"bottleDeposits": 0
},
"attributes": {
"engraving": {
"hasEngraving": false,
"fee": 0,
"location": null,
"lines": []
},
"giftCard": {
"sender": null,
"message": null,
"recipients": [],
"sendDate": null
}
}
},
{
"id": "33b5e5a7-5dd7-4e33-b8a8-5cf7cec79f39",
"fulfillmentId": "65ba501eedcba0987654321f",
"retailerId": "65ba501eedcba0987654321f",
"variantId": "65ba501eedcba0987654321f",
"liquidId": "65ba501eedcba0987654321f",
"legacyGrouping": "GROUPING-123456",
"legacyPid": "pid-abc",
"customerPlacement": "standard",
"isPresale": false,
"estimatedShipBy": null,
"product": {
"name": "Liquid Elixir Aged",
"brand": "Liquid",
"upc": "001928391838291",
"sku": "65ba4f9210fedcba98765432_65ba501eedcba0987654321f",
"mskus": [
"LIQUID-ELIXIR-AGED"
],
"category": "SPIRITS > WHISKEY > BOURBON",
"size": "750 ML",
"volume": "750",
"uom": "ML",
"proof": null,
"attributes": {
"pack": false,
"packDescription": null,
"abv": "40",
"container": "BOTTLE",
"containerType": "Bottle"
}
},
"image": "https://assets.liquidcommerce.co/core/white_matte_bottle_nobg.png",
"pricing": {
"price": 4499,
"unitPrice": 4499,
"quantity": 1,
"tax": 298,
"bottleDeposits": 0
},
"attributes": {
"engraving": {
"hasEngraving": false,
"fee": 5000,
"location": "Side of the Bottle",
"lines": []
},
"giftCard": {
"sender": null,
"message": null,
"recipients": [],
"sendDate": null
}
}
}
]
}
}path
string
API path
version
string
API version used for the request
promoCode
string
Promotional code to apply to the cart
isLegacy
boolean
Whether to return legacy identifiers
refresh
boolean
When true, a new access token will be generated
fulfillmentId
string
ID of the retailer's fulfillment method
engravingLines
array<string>
Array of engraving text lines (if applicable)
scheduledFor
string
ISO date string for scheduled delivery (onDemand only)
sku
string
Product SKU
cart
Cart details
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
const presaleProducts = await liquidCommerce.catalog.search({
filters: [{
key: ENUM_FILTER_KEYS.PRESALE,
values: ENUM_BINARY_FILTER.YES
}],
loc: {
address: {
one: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001'
}
}
});
// Extract presale information from products
presaleProducts.products.forEach(product => {
product.sizes.forEach(size => {
const presale = size.attributes?.presale;
if (presale?.isActive) {
console.log({
product: product.name,
availableFrom: presale.canPurchaseOn,
estimatedShipping: presale.estimatedShipBy,
status: 'Active Presale'
});
}
});
});interface ICart {
id: string;
// ... standard cart fields
// Presale-specific fields
isPresaleLocked: boolean; // Indicates cart contains presale items
presaleExpiresAt: string | null; // Reservation expiration time
events: ICartEvent[]; // Contains presale-related events
}const cartResponse = await liquidCommerce.cart.update({
id: cartId, // Use existing cart ID or create new
items: [{
partNumber: 'WHISKEY_PRESALE_2024_retailer_abc',
quantity: 1,
fulfillmentId: 'shipping_fulfillment_123'
}],
loc: {
address: {
one: '456 Oak Avenue',
city: 'Los Angeles',
state: 'CA',
zip: '90001'
}
}
});
// Check if presale was successfully added
if (cartResponse.isPresaleLocked) {
console.log('Presale item reserved successfully');
console.log(`Complete checkout by: ${cartResponse.presaleExpiresAt}`);
}// Available presale events
enum CART_EVENT_ENUM {
PRESALE_ITEMS_NOT_ALLOWED = 'PresaleItemsNotAllowed',
PRESALE_LIMIT_EXCEEDED = 'PresaleLimitExceeded',
PRESALE_NOT_STARTED = 'PresaleNotStarted',
PRESALE_EXPIRED = 'PresaleExpired',
PRESALE_MIXED_CART = 'PresaleMixedCart',
}function handlePresaleEvents(cart: ICart) {
cart.events.forEach(event => {
switch(event.type) {
case CART_EVENT_ENUM.PRESALE_NOT_STARTED:
// Presale hasn't begun yet
alert('This presale has not started. Please check back later.');
break;
case CART_EVENT_ENUM.PRESALE_LIMIT_EXCEEDED:
// Requested quantity exceeds available inventory
alert('Sorry, not enough inventory available. Please try a smaller quantity.');
break;
case CART_EVENT_ENUM.PRESALE_EXPIRED:
// Presale period has ended
alert('This presale has ended.');
break;
case CART_EVENT_ENUM.PRESALE_MIXED_CART:
// Cannot mix presale with other items
alert('Presale items must be purchased separately.');
break;
}
});
}async function checkoutPresaleCart(cartId: string) {
// Step 1: Prepare checkout
const checkoutPrep = await liquidCommerce.checkout.prepare({
cartId: cartId,
customer: {
firstName: "Sarah",
lastName: "Johnson",
email: "[email protected]",
phone: "3105551234",
birthDate: "1985-06-15"
},
billingAddress: {
firstName: "Sarah",
lastName: "Johnson",
email: "[email protected]",
phone: "3105551234",
one: "789 Pine Street",
two: "Suite 100",
city: "San Francisco",
state: "CA",
zip: "94102"
},
hasSubstitutionPolicy: true,
marketingPreferences: {
canEmail: true,
canSms: false
}
});
// Step 2: Process payment
// Assuming payment element is already mounted
const paymentToken = await liquidCommerce.payment.generateToken();
if ('id' in paymentToken) {
// Step 3: Complete checkout
const order = await liquidCommerce.checkout.complete({
token: checkoutPrep.token,
payment: paymentToken.id
});
return order;
}
}class PresaleCheckoutManager {
private expirationTimer?: NodeJS.Timeout;
startCheckout(cart: ICart) {
if (!cart.isPresaleLocked || !cart.presaleExpiresAt) {
return;
}
const expiresAt = new Date(cart.presaleExpiresAt);
const now = new Date();
const timeRemaining = expiresAt.getTime() - now.getTime();
// Show countdown to user
this.displayCountdown(timeRemaining);
// Warn before expiration
if (timeRemaining > 60000) { // More than 1 minute
this.expirationTimer = setTimeout(() => {
this.warnUserAboutExpiration();
}, timeRemaining - 60000);
}
}
private displayCountdown(milliseconds: number) {
const minutes = Math.floor(milliseconds / 60000);
const seconds = Math.floor((milliseconds % 60000) / 1000);
console.log(`Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')}`);
}
private warnUserAboutExpiration() {
alert('Your presale reservation expires in 1 minute!');
}
cleanup() {
if (this.expirationTimer) {
clearTimeout(this.expirationTimer);
}
}
}// Display presale information prominently
function displayPresaleInfo(product: IProduct) {
const presaleInfo = product.sizes[0]?.attributes?.presale;
if (presaleInfo?.isActive) {
return {
status: 'PRESALE',
availableDate: presaleInfo.canPurchaseOn,
shippingDate: presaleInfo.estimatedShipBy,
message: `Pre-order now, ships ${formatDate(presaleInfo.estimatedShipBy)}`
};
}
}// Check before adding items to existing cart
async function addToCart(item: ICartUpdateItem, existingCartId?: string) {
if (existingCartId) {
// Check if existing cart has presale items
const existingCart = await liquidCommerce.cart.get(existingCartId);
if (existingCart.isPresaleLocked) {
// Create new cart for non-presale items
console.log('Creating new cart - existing cart contains presale items');
return await liquidCommerce.cart.update({
id: 'new',
items: [item],
loc: existingCart.loc
});
}
}
// Safe to use existing cart
return await liquidCommerce.cart.update({
id: existingCartId || 'new',
items: [item],
loc: { /* location */ }
});
}async function robustPresalePurchase(
partNumber: string,
quantity: number,
maxRetries: number = 3
) {
let attempts = 0;
while (attempts < maxRetries) {
try {
const cart = await liquidCommerce.cart.update({
id: 'new',
items: [{ partNumber, quantity, fulfillmentId: 'shipping_123' }],
loc: { /* location */ }
});
// Check for presale events
const hasPresaleError = cart.events.some(e =>
e.type === CART_EVENT_ENUM.PRESALE_LIMIT_EXCEEDED ||
e.type === CART_EVENT_ENUM.PRESALE_EXPIRED
);
if (hasPresaleError) {
throw new Error('Presale not available');
}
if (cart.isPresaleLocked) {
return { success: true, cart };
}
} catch (error) {
attempts++;
if (attempts >= maxRetries) {
return { success: false, error: error.message };
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}class PresaleProductPage {
async displayProduct(productId: string) {
// Fetch product details
const availability = await liquidCommerce.catalog.availability({
ids: [productId],
loc: this.getUserLocation()
});
const product = availability.products[0];
const presaleSize = product.sizes.find(s =>
s.attributes?.presale?.isActive
);
if (presaleSize) {
this.showPresaleBadge();
this.showEstimatedShipping(presaleSize.attributes.presale.estimatedShipBy);
this.enablePresalePurchase(presaleSize);
}
}
private async enablePresalePurchase(size: IProductSize) {
// Set up purchase button
const purchaseButton = document.getElementById('purchase-btn');
purchaseButton.onclick = async () => {
const cart = await liquidCommerce.cart.update({
id: 'new',
items: [{
partNumber: size.variants[0].partNumber,
quantity: 1,
fulfillmentId: size.variants[0].fulfillments[0]
}],
loc: this.getUserLocation()
});
if (cart.isPresaleLocked) {
// Redirect to checkout
window.location.href = `/checkout?cartId=${cart.id}`;
}
};
}
}async function createPresaleCollection() {
// Fetch all active presales
const presales = await liquidCommerce.catalog.search({
filters: [{
key: ENUM_FILTER_KEYS.PRESALE,
values: ENUM_BINARY_FILTER.YES
}],
orderBy: ENUM_ORDER_BY.PRICE,
orderDirection: ENUM_NAVIGATION_ORDER_DIRECTION_TYPE.ASC,
page: 1,
perPage: 20,
loc: { /* user location */ }
});
// Group by availability date
const groupedPresales = presales.products.reduce((acc, product) => {
product.sizes.forEach(size => {
const presale = size.attributes?.presale;
if (presale?.isActive && presale.canPurchaseOn) {
const date = new Date(presale.canPurchaseOn).toDateString();
if (!acc[date]) acc[date] = [];
acc[date].push({ product, size });
}
});
return acc;
}, {});
return groupedPresales;
}hasSubstitutionPolicy
boolean
Whether substitution policy is accepted
isGift
boolean
When the order is set as gift (isGift is set to true), the system adds gift messaging, special packaging, and gift receipts with optional sender anonymity
hasAgeVerify
boolean
Should the checkout verify age
billingSameAsShipping
boolean
Whether billing address is same as shipping
giftOptions
Gift options if applicable
marketingPreferences
Marketing preferences
deliveryTips
array<>
Array of delivery tip objects
deliveryInstructions
array<>
Delivery instructions for on-demand orders only
shippingAddressTwo
string
Optional second line for the shipping address
acceptedAccountCreation
boolean
Whether customer accepted account creation
scheduledDelivery
string
Scheduled delivery datetime (ISO format)
payment
string
Payment method identifier
promoCode
string
Promotional code to apply a discount to the checkout
giftCards
array<string>
Array of gift card codes to apply as payment methods
isLegacy
boolean
Whether to return legacy identifiers
refresh
boolean
When set to true, a new access token will be generated and returned
checkout
Checkout type
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
cartId
string
ID of the cart to checkout
customer
Customer information
billingAddress
Billing address details
statusCode
number
Internal status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
upcs
array<string>
Array of UPCs to check availability. Maximum: 70 UPCs
grouping
array<string>
Array of grouping IDs to check availability. Maximum: 70 grouping IDs
ids
array<string>
Array of product IDs to check availability. Maximum: 70 IDs
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
curl --location 'https://staging.api.liquidcommerce.cloud/checkout/prepare' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
"cartId": "cart_abc123xyz789", // create a new cart
"customer": {
"firstName": "Joe",
"lastName": "John",
"email": "[email protected]",
"phone": "9732119920",
"birthDate": "11-22-1998",
"hasAgeVerify": true
},
"billingAddress": {
"firstName": "NewJoe",
"lastName": "NewJohn",
"email": "[email protected]",
"phone": "1234324987",
"one": "22 Washington Ave",
"two": "",
"city": "Cliffside Park",
"state": "NJ",
"zip": "07010"
},
"hasSubstitutionPolicy": true,
"isGift": false,
"billingSameAsShipping": true,
"giftOptions": {
"message": "",
"recipient": {
"name": "",
"phone": "",
"email": ""
}
},
"marketingPreferences": {
"canEmail": true,
"canSms": true
},
"deliveryTips": [
{
"fulfillmentId": "6570c3ec700628ce1910c105",
"tip": 2500
}
],
"refresh": true
}'{
"statusCode": 200,
"message": "Prepare a checkout prior to processing.",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731613340101,
"timezone": "UTC",
"requestId": "req_abc123xyz789",
"path": "/api/checkout/prepare",
"version": "1.7.0"
},
"auth": {
"token": "auth_token_abc123xyz789",
"type": "ACCESS_TOKEN",
"exp": 1731679155851
},
"checkout": {
"token": "checkout_token_abc123xyz789",
"cartId": "cart_abc123xyz789",
"customer": {
"id": "usr_abc123xyz789",
"email": "[email protected]",
"firstName": "John",
"company": "",
"lastName": "Doe",
"phone": "(555) 555-5555",
"profileImage": "",
"birthDate": "1998-11-22T00:00:00.000Z",
"createdAt": "2024-11-14T19:13:34.231Z",
"updatedAt": "2024-11-14T19:42:20.314Z"
},
"createdAt": "2024-11-14T19:13:35.004Z",
"updatedAt": "2024-11-14T19:42:20.420Z",
"hasAgeVerify": false,
"hasSubstitutionPolicy": true,
"acceptedAccountCreation": false,
"isGift": false,
"billingSameAsShipping": true,
"giftOptions": {
"message": "",
"recipient": {
"name": "",
"email": "",
"phone": ""
}
},
"marketingPreferences": {
"canEmail": true,
"canSms": true
},
"shippingAddress": {
"one": "100 madison ave",
"two": "",
"city": "New york",
"state": "NY",
"zip": "10016",
"country": "US"
},
"billingAddress": {
"one": "100 madison ave",
"two": "",
"city": "New york",
"state": "NY",
"zip": "10016",
"email": "[email protected]",
"country": "US",
"firstName": "John",
"lastName": "Doe",
"company": "",
"phone": "(555) 555-5555"
},
"amounts": {
"subtotal": 5999,
"engraving": 0,
"service": 0,
"shipping": 1599,
"delivery": 0,
"platform": 499,
"discounts": 0,
"giftCards": 0,
"tax": 684,
"tip": 0,
"total": 8781,
"details": {
"discounts": {
"products": 0,
"delivery": 0,
"shipping": 0,
"engraving": 0,
"service": 0
},
"taxes": {
"bag": 5,
"bottleDeposits": 5,
"retailDelivery": 0,
"products": 532,
"delivery": 0,
"shipping": 142
}
}
},
"items": [
{
"variantId": "var_abc123xyz789",
"liquidId": "liq_abc123xyz789",
"cartItemId": "item_abc123xyz789",
"retailerId": "ret_abc123xyz789",
"fulfillmentId": "ful_abc123xyz789",
"name": "Whispering Angel Rosé",
"brand": "Chateau D'esclans",
"mainImage": "https://storage.example.com/images/products/image1.png",
"image": "https://storage.example.com/images/products/image1.png",
"size": "1.5 L",
"pack": false,
"packDesc": "",
"volume": "1.5",
"uom": "LITRE",
"abv": "14",
"containerType": "Bottle",
"container": "Bottle",
"partNumber": "88320002003_ret_abc123xyz789",
"upc": "88320002003",
"catPath": "WINE > ROSE WINE",
"customerPlacement": "standard",
"price": 5999,
"unitPrice": 5999,
"quantity": 1,
"attributes": {
"giftCard": {
"sender": "",
"message": "",
"recipients": [],
"sendDate": ""
},
"engraving": {
"isEngravable": true,
"hasEngraving": false,
"maxCharsPerLine": 0,
"maxLines": 0,
"fee": 0,
"location": "",
"lines": []
}
},
"bottleDeposits": 5,
"tax": 532
}
],
"retailers": [
{
"id": "ret_abc123xyz789",
"name": "East Houston St Wine & Liquors",
"address": {
"one": "250 E Houston Street",
"two": "",
"city": "New York",
"state": "NY",
"zip": "10002",
"country": "US"
},
"subtotal": 5999,
"engraving": 0,
"service": 0,
"shipping": 1599,
"delivery": 0,
"platform": 499,
"discounts": 0,
"giftCards": 0,
"tax": 684,
"tip": 0,
"total": 8781,
"details": {
"discounts": {
"products": 0,
"delivery": 0,
"shipping": 0,
"engraving": 0,
"service": 0
},
"taxes": {
"bag": 5,
"bottleDeposits": 5,
"retailDelivery": 0,
"products": 532,
"delivery": 0,
"shipping": 142
}
},
"fulfillments": [
{
"id": "ful_abc123xyz789",
"scheduledFor": "",
"type": "shipping",
"expectation": {
"detail": "Ships in 2-3 days",
"short": "2-3 days"
},
"subtotal": 5999,
"engraving": 0,
"service": 0,
"shipping": 1599,
"delivery": 0,
"discounts": 0,
"giftCards": 0,
"tip": 0,
"items": [
"item_abc123xyz789"
],
"total": 8282,
"details": {
"discounts": {
"products": 0,
"delivery": 0,
"shipping": 0,
"engraving": 0,
"service": 0
},
"taxes": {
"bag": 5,
"bottleDeposits": 5,
"retailDelivery": 0,
"products": 532,
"delivery": 0,
"shipping": 142
}
},
"tax": 684
}
]
}
],
"payment": ""
}
}{
"statusCode": 200,
"message": "List of available products based on upcs provided.",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731607501591,
"timezone": "UTC",
"requestId": "requestid_123abc456def",
"path": "/api/catalog/availability",
"version": "1.7.0"
},
"products": [
{
"id": "ful_def456uvw123",
"salsifyGrouping": "GRP-78392",
"name": "Maestro Dobel® Diamante Tequila Cristalino",
"brand": "Maestro Dobel",
"catPath": "Spirits > Tequila > Cristalino",
"category": "",
"classification": "",
"type": "",
"subType": "",
"region": "Jalisco",
"country": "Mexico",
"material": "Agave",
"color": "White",
"flavor": "",
"variety": "100% Blue Agave",
"appellation": "",
"abv": "40",
"proof": "80",
"age": "",
"vintage": "",
"description": "Maestro Dobel® Diamante® is the original Cristalino. Blended extra Añejo, Añejo and reposado tequilas are aged in Balkan new white wood barrels, then filtered again to retain an elegant flavor. The Cristalino tequila has a mild oak aroma with a touch of vanilla. It has a citrus and prickly pear flavor with a smooth, crisp, clean finish. Pour one shot of straight Diamante Tequila to sip, into an old-fashioned glass with ice and a lemon slice, or as a Margarita. (80 proof)",
"htmlDescription": "<p>Maestro Dobel® Diamante® is the original Cristalino. Blended extra Añejo, Añejo and reposado tequilas are aged in Balkan new white wood barrels, then filtered again to retain an elegant flavor. The Cristalino tequila has a mild oak aroma with a touch of vanilla. It has a citrus and prickly pear flavor with a smooth, crisp, clean finish. Pour one shot of straight Diamante Tequila to sip, into an old-fashioned glass with ice and a lemon slice, or as a Margarita. (80 proof)</p>",
"tastingNotes": "World's first multi-aged clear tequila.",
"images": [
"https://example.com/products/dobel-diamante-1.jpg",
"https://example.com/products/dobel-diamante-2.jpg"
],
"sizes": [
{
"id": "sz_xy789zw456",
"upc": "00811538012034",
"size": "750 ML",
"volume": "750",
"uom": "MILLILITRE",
"container": "Bottle",
"containerType": "Bottle",
"pack": false,
"packDesc": "",
"image": "https://example.com/products/dobel-diamante-bottle.jpg",
"attributes": {
"engraving": {
"status": true,
"maxLines": 3,
"maxCharsPerLine": 16,
"fee": 5000,
"location": "Back of the Bottle"
}
}
}
]
},
{
"id": "ful_ghi789rst123",
"salsifyGrouping": "649066c19661fb45f6869934",
"name": "The Macallan Double Cask 12 Years Old Single Malt Whisky",
"brand": "The Macallan",
"catPath": "Spirits > Whiskey > Scotch",
"category": "",
"classification": "",
"type": "",
"subType": "",
"region": "Speyside",
"country": "Scotland",
"material": "Grain",
"color": "Tawny/brown",
"flavor": "",
"variety": "Barley",
"appellation": "Speyside",
"abv": "40",
"proof": "80",
"age": "",
"vintage": "",
"description": "The Macallan Double Cask 12-Year-Old pairs the indulgent fruit, caramel, and oak spice character of Sherry-seasoned European oak with the bright citrus and vanilla notes of Sherry-seasoned American oak for a satisfyingly rich and perfectly balanced flavor experience. Awarded unanimous Double Gold upon release.",
"htmlDescription": "<p>The Macallan Double Cask 12-Year-Old pairs the indulgent fruit, caramel, and oak spice character of Sherry-seasoned European oak with the bright citrus and vanilla notes of Sherry-seasoned American oak for a satisfyingly rich and perfectly balanced flavor experience. Awarded unanimous Double Gold upon release.</p>",
"tastingNotes": "\"If there is royalty in the whisky world, it belongs to Scotland, and if there is a king of Scotch whisky, it's The Macallan.\" – Forbes",
"images": [
"https://example.com/products/macallan-12-1.jpg"
],
"sizes": [
{
"id": "sz_pqr123mn456",
"upc": "00812066021598",
"size": "750 ML",
"volume": "750",
"uom": "MILLILITRE",
"container": "Bottle",
"containerType": "Bottle",
"pack": false,
"packDesc": "",
"image": "https://example.com/products/macallan-12-bottle.jpg",
"attributes": {
"engraving": {
"status": true,
"maxLines": 3,
"maxCharsPerLine": 16,
"fee": 5000,
"location": "Below the Label"
}
}
}
]
}
],
"retailers": [
{
"id": "ret_abc123xyz789",
"name": "LiquidCommerce Wine & Spirits",
"platformFee": 499,
"address": {
"one": "240 Loisaida Avenue",
"two": "",
"city": "New York",
"state": "NY",
"zip": "10009",
"country": "US"
},
"fulfillments": [
{
"id": "ful_ghi189rst123",
"timezone": "America/New_York",
"type": "onDemand",
"canEngrave": false,
"fees": {
"min": 1999,
"fee": 0,
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Arrives in 60 mins",
"short": "60 mins"
},
"hours": {
"monday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"tuesday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"wednesday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"thursday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"friday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"saturday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"sunday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
}
},
"breaks": [],
"items": []
}
]
},
{
"id": "ret_abc123xyz189",
"name": "LiquidCommerce Barn",
"platformFee": 499,
"address": {
"one": "1000 N 5th Ave",
"two": "134",
"city": "Vernon Hills",
"state": "NY",
"zip": "10061",
"country": "US"
},
"fulfillments": [
{
"id": "ful_abc123xyz189",
"timezone": "America/Chicago",
"type": "shipping",
"canEngrave": true,
"fees": {
"pack": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"individual": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Ships in 2-3 days",
"short": "2-3 days"
},
"hours": {
"monday": {
"active": false,
"times": []
},
"tuesday": {
"active": false,
"times": []
},
"wednesday": {
"active": false,
"times": []
},
"thursday": {
"active": false,
"times": []
},
"friday": {
"active": false,
"times": []
},
"saturday": {
"active": false,
"times": []
},
"sunday": {
"active": false,
"times": []
}
},
"breaks": [],
"items": []
}
]
}
]
}curl --location 'https://staging.api.liquidcommerce.cloud/catalog/availability' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"upcs": [
"00812066021598",
"00619947000020",
"00081753833916",
"00083085904081"
],
"grouping": [
"649066c19661fb45f6869934",
"GROUPING-39553",
"649066c19661fb45f6869937"
],
"loc": {
"address": {
"one": "100 madison ave",
"two": "apt 1707",
"city": "New york",
"state": "NY",
"zip": "10016"
}
},
"shouldShowOffHours": false,
"isLegacy": true,
"refresh": false,
"isLean": false
}'Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
path
string
API path
version
string
API version used for the request
loc
Location object for determining availability. If no address is provided there will not be availability for the requested products.
shouldShowOffHours
boolean
When true, returns onDemand retailers outside their operating hours
isLegacy
boolean
Whether to return legacy identifiers
isLean
boolean
Whether to return a minimal response format
refresh
boolean
When set to true, a new access token will be generated and returned
navigation
Navigation schema
products
array<productType>
Array of matched products
retailers
array<retailerType>
Array of available retailers
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
data
Array<>
Array of address objects
zip
string
zip code
country
string
country in which the address is located
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
id
string
Google Places API location identifier for the selected address
key
string
Your Google Places API key
refresh
boolean
When set to true, a new access token will be generated and returned
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
formattedAddress
string
Formatted address
coords
Address coordinates
address
Address info object
one
string
Street address
two
string
Street address
city
string
City address
state
string
state, you can use either 2 letter code or the full name
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
platformFee: number [optional]
LiquidCommerce application fee per retailer
address: [optional]
location information split out to individual values for the street, city, state, zip code, country, latitude and longitude
fulfillments: Array<>
retailer available fulfillment configurations
id: string [optional]
identifier of the address
one: string
retailer street address
two: string
retailer apt, suite, floor, etc
city: string
retailer city
state: string
retailer state, you can use either 2 letter code or the full name
id: string
fulfillment identifier
type:
fulfillment type, ex: onDemand | shipping
canEngrave: boolean
indicates whether the retailer support engraving services
deliveryFee: number [optional]
specific delivery fee for this fulfillment
shippingFee: number [optional]
specific shipping fee for this fulfillment
min: number
the minimum subtotal that must be met for the order to be placed
fee: number
the fee for the fulfillment service, ex: 1500
free:
conditions for free delivery
pack:
the shipping fee configurations for pack items
individual:
the shipping fee configurations for individual items
free:
conditions for free delivery
fee: number
The fee for the fulfillment service, ex: 1500
active: boolean
The status of of the fulfillment shipping method
min: number
The min item total required to checkout
maxQuantity: number
The maximum number of products allowed
active: boolean
whether free delivery is available
min: number
minimum purchase amount for free delivery
detail: string
detailed expectation of fulfillment, ex: Ships in 3 days
short: string
short expectation of fulfillment, ex: 3 days
engraving: string
detailed expectation of engravable shipping fulfillment, ex: Ships in 10 days
[DayOfWeek]:
hours of operations for each day of the week, ex:
active: boolean
whether the day is available for scheduling
times: Array<>
the times available for scheduling, ex:
startsAt: string
the expected start time for the day, ex: 08:30
endsAt: string
the expected end time for the day, ex: 23:00
id: string
retailer identifier
name: string
retailers name, ex: Liquid Wine and Spirits
curl --location 'https://staging.api.liquidcommerce.cloud/address/details' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"id": "placeid_123abc456ef",
"key": "<YOUR_GOOGLE_PLACES_API_KEY>"
}'{
"statusCode": 200,
"message": "Get the longitude and latitude of the address you requested",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731594349056,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/address/details",
"version": "1.7.0"
},
"data": {
"formattedAddress": "100 Madison Avenue, Lakewood, NJ 08701",
"coords": {
"lat": 40.091142,
"long": -74.2169165
},
"address": {
"one": "100 Madison Avenue",
"two": "",
"city": "Lakewood",
"state": "NJ",
"zip": "08701",
"country": "US",
}
```
}
}[
{
"id": "retailerId_123abc123abc",
"name": "Cheers On Demand",
"platformFee": 499,
"address": {
"one": "867 Fillmore St",
"two": "",
"city": "San Francisco",
"state": "CA",
"zip": "94117",
"country": "US"
},
"fulfillments": [
{
"id": "6570c3ec700628ce1910c155",
"timezone": "America/Los_Angeles",
"type": "onDemand",
"canEngrave": false,
"doesAllowPromos": false,
"doesAllowGiftCards": false,
"productTypesAllowed": [],
"fees": {
"min": 2500,
"fee": 0,
"free": {
"active": false,
"min": 2500
}
},
"expectation": {
"detail": "Arrives in 60 mins",
"short": "60 mins"
},
"hours": {
"monday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:30"
}
]
},
"tuesday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:30"
}
]
},
"wednesday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:30"
}
]
},
"thursday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:30"
}
]
},
"friday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:30"
}
]
},
"saturday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:59"
}
]
},
"sunday": {
"active": true,
"times": [
{
"startsAt": "08:00",
"endsAt": "23:59"
}
]
}
},
"breaks": [],
"items": []
}
]
}
]Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
zip: string
retailer zip code
country: string
retailer country where the address is located
lat: number
retailer latitude, ex: 40.744860
long: number
retailer longitude, ex: -73.985314
engravingFee: number [optional]
specific engraving fee for this fulfillment
platformFee: number [optional]
platform fee for this fulfillment
subtotal: number [optional]
Subtotal for items in this fulfillment
timezone: string [optiona]
timezone associated with the fulfillment
fees: FeeShipping | FeeDelivery
fulfillment's fee configurations, if the fulfillment type is shipping(FeeShipping), if the fulfillment type is onDemand(FeeDelivery)
expectation: Expectation
fulfillment's expectation configurations
hours: Hours
fulfillment's hours configurations
breaks: Array<Time>
the breaks within the working hours
items: Array<string>
fulfillment's item ids
doesAllowPromos: boolean
indicates whether the retailer per fulfillment level support promo codes for cart & checkout
doesAllowGiftCards: boolean
indicates whether the retailer per fulfillment level support gift cards for checkout
productTypesAllowed: Array<string>
list of fulfillment categories allowed
enum DAY_OF_WEEK {
MONDAY = 'monday',
TUESDAY = 'tuesday',
WEDNESDAY = 'wednesday',
THURSDAY = 'thursday',
FRIDAY = 'friday',
SATURDAY = 'saturday',
SUNDAY = 'sunday'
}"monday": {
"active": true,
"times": [
{
"startsAt": "00:00",
"endsAt": "23:59"
}
]
}"times": [
{
"startsAt": "00:00",
"endsAt": "23:59"
}
]Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
email
string
User's email address
firstName
string
User's first name
lastName
string
User's last name
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object
id
string
User's identifier
email
string
User's email address
firstName
string
User's first name
lastName
key: string
session key
secret: string
session secret
createdAt: Date
session creation timestamp
{
"statusCode": 201,
"message": "Creating a new session succeeded",
"metadata": {
"languages": [
"en"
],
"timestamp": 1731595854533,
"timezone": "UTC",
"requestId": "reqid_123abc45def",
"path": "/api/users/session",
"version": "1.7.0"
},
"data": {
"id": "usrid_123abc456def",
"email": "[email protected]",
"firstName": "John",
"company": "",
"lastName": "Smith",
"phone": null,
"profileImage": "",
"birthDate": null,
"createdAt": "2024-11-14T13:17:47.482Z",
"updatedAt": "2024-11-14T14:50:54.613Z",
"addresses": [],
"savedPayments": [],
"session": {
"key": "SESSION_KEY",
"secret": "SECRET_KEY",
"createdAt": "2024-11-14T14:50:54.843Z"
}
}
}curl --location 'https://staging.api.liquidcommerce.cloud/users/session' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"email": "[email protected]",
"firstName": "John",
"lastName": "Smith"
}'path
string
API path
version
string
API version used for the request
company
string
User's company name
phone
string
User's phone number in E.164 format
profileImage
string
URL of user's profile image
birthDate
string
User's birth date in YYYY-MM-DD format
id
string
Existing user identifier (for updates only), email becomes optional
data
User session
string
User's last name
phone
string
User's phone number
company
string
User's company
profileImage
string
URL to user's profile image
birthDate
string
Users's birth date
createdAt
date
Account creation timestamp
updatedAt
date
Last update timestamp
addresses
Array<UserAddress>
User's saved addresses
savedPayments
Array<UserPayment>
User's saved payment methods
session
User's current session information
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
Unique identifier for the API request. Used for debugging and support
The SDK requires configuration during initialization:
Before initializing the SDK clients, you'll need a Google Places API Key from your Google Cloud Console. This key is required for location-based features and should be kept secure.
LiquidCommerce Client
LiquidCommerceOrders Client
All API responses follow a consistent structure:
While the SDK handles authentication automatically, you can also manually retrieve the authentication details:
Services for address validation and lookup:
Product catalog search and availability services:
Shopping cart management:
User profile and preferences management:
The Payment Element is a secure and modern UI component for collecting payment details. It simplifies PCI compliance by handling all sensitive information within an iframe.
Before mounting the Payment Element, you must create a payment session. This can be tied to a user, cart, or checkout.
Initialize and Mount
The LiquidCommercePaymentElement function creates and manages the UI component.
Generate a Confirmation Token
Once the user has filled out the payment form, create a confirmation token. This token securely represents the payment details.
Use the new integration that has the UI and API methods decoupled, allowing you to use the SDK entirely server side.
The payment system uses secure elements for handling sensitive payment data. Before using payment features, you must first create a user session.
User Session Creation:
PCI Compliance: The payment element handles card data securely within an iframe, ensuring your application never directly touches sensitive payment information.
Token-Based: All payment data is tokenized - you only receive secure tokens that can't be used to retrieve the original card details.
Single Use: Payment tokens are single-use and expire after a short time period.
Domain Validation: Payment elements will only work on domains that have been pre-registered with your account.
Error Handling: Always implement proper error handling:
Cleanup: Always clean up payment elements when done:
When navigation away from payment page
After successful payment
After failed payment attempt
Before unmounting payment component
Event Handling: Monitor element state for better user experience:
The payment element automatically adapts to:
Mobile and desktop viewports
Right-to-left languages
Dark/light themes
Different container sizes
When testing payments in staging environment, use these test cards:
These cards will be accepted in test mode and will simulate successful payments. They should only be used in the staging environment, never in production.
Important Notes:
These cards work only in test/staging environment
Real cards will be declined in test mode
Test cards will be declined in production
All test transactions use simulated funds
Testing Environment Limitations ⚠️ Important Testing Note When doing and checkout complete on the testing environment, certain transaction amount ranges may be intentionally restricted to simulate payment declines, gateway errors, or other failures. This allows you to verify that your integration can handle error scenarios correctly.
Example — Amounts that may trigger simulated errors:
$2000.00 – $2999.99: Processor Declined
$3000.00 – $3000.99: Processor Network Unavailable
$5001.00: Gateway Rejected (Application Incomplete)
For routine testing, avoid using these ranges unless you specifically want to test error-handling behavior.
Checkout process management:
For direct checkout payments, the flow is similar but uses the checkout session:
Provides secure access to order data throughout the finalized states of the order lifecycle within the LiquidCommerce ecosystem.
Fetch by ID
Webhook Test
The SDK throws errors for various scenarios. Always wrap SDK calls in try-catch blocks:
Common error scenarios:
Authentication failures
Invalid parameters
Network errors
Resource not found
All monetary values in the SDK are handled in cents (the smallest currency unit). For example:
$10.00 is represented as 1000
$5.99 is represented as 599
$0.50 is represented as 50
import LiquidCommerce from '@liquidcommerce/cloud-sdk';
// Your Account token provided to you through
// your account representative
const client = await LiquidCommerce('YOUR_LIQUIDCOMMERCE_API_KEY', {
googlePlacesApiKey: 'YOUR_GOOGLE_PLACES_API_KEY',
env: 'stage' // or 'prod'
});
// Initialize the client
await client.init();import LiquidCommerceOrders from '@liquidcommerce/cloud-sdk';
// Your Account user and passoword will be provided to you
// through your account representative
const orderClient = await LiquidCommerceOrders({
userID: 'YOUR_ORDER_API_USER_ID',
password: 'YOUR_ORDER_API_PASSWORD',
env: LIQUID_COMMERCE_ENV.STAGE, // or PROD
});
// Initialize the client
await orderClient.init();Confirm the Payment Session
Use the confirmation token to finalize the payment and retrieve the payment method details.
Lifecycle Management
Properly manage the element's lifecycle to ensure a smooth user experience and resource cleanup.
Validation errors
const confirmation = await client.user.confirmPaymentSession(confirmationToken);
if (confirmation.data) {
const paymentMethod = confirmation.data;
// Now you have the payment method ID, card details, etc.
// e.g., paymentMethod.id, paymentMethod.card.brand
}// Listen to events
paymentElement.subscribe('ready', () => {
console.log('Payment element is ready.');
});
paymentElement.subscribe('change', (event) => {
// Handle form state changes (e.g., enable/disable submit button)
});
// Clean up when the element is no longer needed
paymentElement.unmount();
paymentElement.destroy();interface ApiResponse<T> {
statusCode: number;
message: string;
metadata: {
languages: string[];
timestamp: number;
timezone: string;
requestId: string;
path: string;
version: string;
};
data?: T; // Present in responses with data
}// Manually retrieve authentication details
try {
const authDetails = await client.auth();
console.log('Access Token:', authDetails.token);
console.log('Expires In:', authDetails.exp);
} catch (error) {
console.error('Failed to get auth details:', error);
}// Address autocompletion
const autocompleteResponse = await client.address.autocomplete({
input: '100 Madison Ave, New York'
});
// Response type: IApiResponseWithData<IAddressAutocompleteResult[]>
// {
// id: string;
// description: string;
// }
// Get detailed address information
const detailsResponse = await client.address.details({
id: 'ChIJd8BlQ2BZwokRjMKtTjMezRw'
});
// Response type: IApiResponseWithData<IAddressDetailsResult>
// {
// formattedAddress: string;
// coords: {
// lat: number;
// long: number;
// }
// address: {
// one: string,
// two: string,
// city": string,
// state: string,
// zip: string,
// country: "US"
// }
// }// Check product availability
const availabilityResponse = await client.catalog.availability({
upcs: ['123456789012', '210987654321'], // UPC codes
grouping: ['group1', 'group2'], // Optional group identifiers
ids: ['id1', 'id2'], // Optional product IDs
loc: {
address: {
one: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001'
}
},
shouldShowOffHours: true
});
// Search catalog with filters
const searchResponse = await client.catalog.search({
search: 'whiskey',
pageToken: "",
entity: "",
page: 1,
perPage: 20,
orderBy: ENUM_ORDER_BY.PRICE,
orderDirection: ENUM_NAVIGATION_ORDER_DIRECTION_TYPE.ASC,
filters: [
{
key: ENUM_FILTER_KEYS.CATEGORIES,
values: [ENUM_SPIRITS.WHISKEY]
},
{
key: ENUM_FILTER_KEYS.PRICE,
values: { min: 2000, max: 10000 } // Prices in cents
},
{
key: ENUM_FILTER_KEYS.AVAILABILITY,
values: ENUM_AVAILABILITY_VALUE.IN_STOCK
}
],
loc: {
address: {
one: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001'
}
}
});// Create new cart
const newCart = await client.cart.get();
// Retrieve existing cart
const existingCart = await client.cart.get('cart_id', true); // Second parameter for refresh
// Update cart
const updatedCart = await client.cart.update({
id: 'cart_id',
items: [
{
partNumber: '123456789012_retailer_id', // Required: {UPC}_{retailerId}
quantity: 2,
fulfillmentId: 'fulfillment_id',
engravingLines: ['Line 1', 'Line 2'], // Optional
scheduledFor: '2024-12-25', // Optional
}
],
loc: {
address: {
one: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001'
}
},
promoCode: 'DISCOUNT10', // Optional
giftCards: ['GC123456'] // Optional
});// Create/update user session
const userSession = await client.user.session({
email: "[email protected]",
firstName: "John",
lastName: "Smith",
phone: "2125551234",
company: "Company Inc",
profileImage: "https://...",
birthDate: "1990-01-01",
id:'user_id', // Existing user identifier (for updates only), email becomes optional
});
// Fetch user by ID or email
const userData = await client.user.fetch('user_id_or_email');
// Address management
const newAddress = await client.user.addAddress({
customerId: 'customer_id',
placesId: 'google_places_id', // Optional if providing address details
one: '100 Madison St',
two: 'Apt 4B',
city: 'New York',
state: 'NY',
zip: '10004',
country: 'US',
lat: 40.7128, // Optional
long: -74.0060, // Optional
type: ENUM_ADDRESS_TYPE.SHIPPING,
isDefault: true
});
const updatedAddress = await client.user.updateAddress({
// Same parameters as addAddress
});
// Payment methods
// 1. First create/retrieve a user session
const userSession = await client.user.session({
email: "[email protected]",
// ... other user details
});
// 2. Initialize payment form with session data
// The session response includes payment configuration in the auth object
await client.payment.mount({
clientSecret: userSession.data.session.setupIntent, // From user session response
key: userSession.data.session.publicKey, // From user session response
elementId: 'payment-element-container',
appearance: {
theme: 'night' // 'default' | 'night' | 'flat'
},
elementOptions: {
layout: 'tabs' // 'tabs' | 'accordion' | 'auto'
}
});
// 3. Listen for payment element events
client.payment.subscribe('ready', () => {
console.log('Payment element is ready');
});
client.payment.subscribe('change', (event) => {
// Handle validation and form state changes
console.log('Payment element changed:', event);
});
// 4. When user submits, generate payment token
const tokenResult = await client.payment.generateToken();
if ('error' in tokenResult) {
console.error('Token generation failed:', tokenResult.error);
// Handle error cases:
// - validation_error: Invalid payment details
// - api_error: Server/API issues
// - client_error: SDK/setup issues
// - confirm_error: Payment confirmation failed
} else {
// Payment method successfully created
console.log('Payment token:', tokenResult.id);
// 5. Add payment method to user profile
await client.user.addPayment({
userId: userSession.data.id,
paymentMethodId: tokenResult.id,
isDefault: true
});
}
// 6. Clean up
client.payment.unmount();
client.payment.destroy();
// Data removal
await client.user.purge('user_id_or_email');
await client.user.purgeAddress('address_id');
await client.user.purgePayment('user_id', 'payment_id');// Create a payment session to get a client secret
const paymentSession = await client.user.paymentSession({
// Optionally link to a cart, checkout, or customer
// cartId: 'your_cart_id',
// checkoutToken: 'your_checkout_token',
// customerId: 'your_customer_id',
});
const { key, secret } = paymentSession.data.session;// Initialize the payment element
const paymentElement = LiquidCommercePaymentElement({
session: {
key, // Public key from payment session
secret, // Client secret from payment session
},
});
// Mount the element to a container in your DOM
await paymentElement.mount({
elementId: 'payment-element-container',
appearance: {
theme: 'night', // 'stripe' | 'night' | 'flat'
},
elementOptions: {
layout: 'tabs', // 'tabs' | 'accordion' | 'auto'
},
});const result = await paymentElement.createConfirmationToken();
if (result.token) {
// Token successfully created
const confirmationToken = result.token;
// Use this token to complete the checkout or save the payment method
} else {
// Handle error
console.error(result.message);
}// First create or get a user session
const userSession = await client.user.session({
email: "[email protected]",
// ... other user details
});
// The session response includes necessary payment credentials
const { secret, key } = userSession.data.session;// Initialize payment form using session credentials
await client.payment.mount({
clientSecret: secret, // Required: From user session response
key, // Required: From user session response
elementId: 'payment-element-container', // Your DOM element ID
appearance: {
theme: 'night' // 'default' | 'night' | 'flat'
},
elementOptions: {
layout: 'tabs' // 'tabs' | 'accordion' | 'auto'
}
});
// Monitor payment element state
client.payment.subscribe('ready', () => {
// Element is ready to accept input
});
client.payment.subscribe('change', (event) => {
const { complete, empty, value } = event;
// Handle validation state changes
});
// Process payment when ready
const tokenResult = await client.payment.generateToken();
// Handle the result
if ('error' in tokenResult) {
const { type, message, code } = tokenResult.error;
// type can be: 'validation_error' | 'api_error' | 'client_error' | 'confirm_error'
} else {
// Use tokenResult.id for checkout completion or saving payment method
const { id, card } = tokenResult;
}
// Always clean up when done
client.payment.unmount();
client.payment.destroy();try {
const token = await client.payment.generateToken();
if ('error' in token) {
switch(token.error.type) {
case 'validation_error':
// Handle invalid card data
break;
case 'api_error':
// Handle API/network issues
break;
case 'client_error':
// Handle setup/configuration issues
break;
case 'confirm_error':
// Handle payment confirmation failures
break;
}
}
} catch (error) {
// Handle unexpected errors
}client.payment.subscribe('change', (event) => {
// Update UI based on validation state
const { complete, empty } = event;
submitButton.disabled = !complete || empty;
});
client.payment.subscribe('loaderror', (event) => {
// Handle element loading failures
console.error('Payment element failed:', event.error);
});// Test Visa Card
Card Number: 4242 4242 4242 4242
Expiry: Any future date
CVC: Any 3 digits
ZIP: Any 5 digits
// Test Mastercard
Card Number: 5555 5555 5555 4444
Expiry: Any future date
CVC: Any 3 digits
ZIP: Any 5 digits
// Example test card usage:
/*
Card: 4242 4242 4242 4242
Expiry: 12/29
CVC: 123
ZIP: 10001
*/// Prepare checkout
const preparedCheckout = await client.checkout.prepare({
cartId: "cart_id",
customer: {
id: "customer_id", // Optional
email: "[email protected]", // Required
firstName: "John",
lastName: "Smith",
phone: "2125551234",
birthDate: "1990-01-01"
},
hasAgeVerify: true,
billingAddress: {
firstName: "John",
lastName: "Smith",
email: "[email protected]",
phone: "2125551234",
one: "123 Main St",
two: "Apt 4B",
city: "New York",
state: "NY",
zip: "10001",
country: "US"
},
hasSubstitutionPolicy: true,
isGift: true,
billingSameAsShipping: false,
giftOptions: {
message: "Happy Birthday!",
recipient: {
name: "Jane Smith",
email: "[email protected]",
phone: "2125555678"
}
},
marketingPreferences: {
canEmail: true,
canSms: true
},
deliveryTips: [
{
fulfillmentId: "fulfillment_id",
tip: 500 // Amount in cents
}
],
deliveryInstructions: [
{
fulfillmentId: 'fulfillment_id',
instructions: "", // 250 Max characters
},
],
acceptedAccountCreation: true,
scheduledDelivery: "2024-12-25T14:00:00Z",
promoCode: 'DISCOUNT10', // Optional
giftCards: ['GC123456'], // Optional
});
// Complete checkout
const completedCheckout = await client.checkout.complete({
token: preparedCheckout.token,
payment: "payment_id"
});// 1. First prepare the checkout
const preparedCheckout = await client.checkout.prepare({
cartId: 'cart_id',
// ... other checkout details
});
// 2. Initialize payment element with checkout session
const paymentElement = LiquidCommercePaymentElement({
session: {
key: preparedCheckout.data.payment.publicKey, // From checkout prepare response
secret: preparedCheckout.data.payment.clientSecret, // From checkout prepare response
}
});
// 3. Mount the element
await paymentElement.mount({
elementId: 'payment-element-container',
appearance: { theme: 'night' },
elementOptions: { layout: 'tabs' },
});
// 4. Handle payment element events and create confirmation token
const result = await paymentElement.createConfirmationToken();
if (result.token) {
// 5. Confirm the payment collected with the confirmation token
const confirmation = await client.user.confirmPaymentSession(confirmationToken);
if (confirmation?.data?.id) {
// 6. Complete checkout with the confirmation token
const completedCheckout = await client.checkout.complete({
token: preparedCheckout.data.token,
payment: confirmation?.data?.id,
});
}
}
// 7. Clean up
paymentElement.unmount();
paymentElement.destroy();const orderClient = await LiquidCommerceOrders({
userID: 'YOUR_ORDER_API_USER_ID',
password: 'YOUR_ORDER_API_PASSWORD',
env: LIQUID_COMMERCE_ENV.STAGE, // or PROD
});
// Fetch order details by ID or number
const orderResponse = await orderClient.order.fetch(/* reference id or order number */);// Test webhook endpoint
const webhookTestResult = await client.webhook.test(/* endpoint */);
// Response is a simple boolean indicating success or failure
// true = webhook test was successful
// false = webhook test failedtry {
const result = await client.someMethod();
} catch (error) {
console.error('Operation failed:', error.message);
// Handle specific error cases
}A full reference to a product type for different Liquid Commerce APIs.
id: string
identifier of the product
name: string
each size's container type, ex: Glass
pack: boolean
whether the size is a pack, ex: true
packDesc: string
if the size is a pack and a description is available, ex: 8pk
volume: string
volume measurement
uom: string
unit of measure
image: string
product size specific image (if available, otherwise defaults to the mainImage)
attributes:
each size's attributes
modalities: [optional]
available fulfillment modalities
variants: Array<>
each retailer's available variant for the size
product awards
recipes: Array<>
related recipes
video: Array<>
product videos
tastingNotes: Array<>
detailed tasting notes
personalizations: Array<>
personalization options
engraving location on the product, ex: Above the label
variant sale price, ex: 1199 ($11.99)
stock: number
variant stock quantity, ex: 82
fulfillmentTypes:
variants retailer offered fulfillment based on type
fulfillments: string[]
fulfillment identifier (used to map the Array<>),
ex: ["6570c3ec700628ce1910c265", "6565f4e8700628ce19106b14"]
height for personalization area
image: string
personalization reference image url
fee: number
cost of personalization
availableFrom: Date
start date for personalization availability
availableTo: Date
end date for personalization availability
product name
salsifyGrouping: string [optional]
optional Salsify grouping identifier
brand: string
product brand
catPath: string
the category path normalized through our liquid taxonomy
category: string
normalized category
classification: string
product classification, ex: Canadian Whiskey
type: string
product type, ex: Whiskey
subType: string
product type, ex: Flavored Whiskey
region: string
product region, ex: Toronto
country: string
country of origin, ex: Canada
material: string
material used in production, ex: Grains
color: string
product color, ex: Tawny/Brown
flavor: string
product flavor profile, ex: Apple
variety: string
product variety, ex: Blend-Grains
appellation: string
similar to country or region, ex: Canada
abv: string
product alcohol by volume, ex: 40 (40%)
proof: string
alcohol proof
age: string
product age, ex: 14
vintage: string
year of production/harvest, ex: 2020
description: string
a product detailed description, max length: 250 characters
htmlDescription: string
a product detailed description with html tags, max length: 250 characters
tastingNotes: string
product tasting notes breakdown, max length: 250 characters
additionalInformation: string [optional]
a product additional information
images: string[] | Array<Record<string, any>>
product images
sizes: Array<Size>
all of the available sizes for the product
attributes: Partial<Attributes> [optional]
optional product attributes
priceInfo: ProductPriceInfo | null
represents product price information
id: string
identifier of the product size
size: string
product size, ex: 750ML
salsifyPid: string [optional]
optional Salsify product identifier
upc: string
each size's UPC
container: string
each size's container, ex: Bottle
engraving: ProductSizeEngraving
Each size's engraving attributes
presale: ProductPresale
Representing a product presale
maxQuantityPerOrder: number [optional]
Maximum quantity per order allowed for this product
canPurchaseOn: Date | null
The date when the product can be added to the cart
estimatedShipBy: Date | null
The date when the product is expected to ship
isActive: boolean
Indicates whether the presale is currently active
language: string
The language associated with the product presale
brandOrigin: string
brand's origin information
originStatement: string
detailed origin description
ownershipType: Array<string>
types of ownership
tags: Array<string>
product tags
images: AttributesImage
product image configurations
status: boolean
whether engraving is activated if true
validated: boolean [optional]
engraving validation
maxLines: number
the maximum number of line allowed, ex: the maximum allowed in this example id 2 (maxLines: 2) [ "Happy Birthday", "100 more!" ]
maxCharsPerLine: number
the maximum number of characters allowed per line, ex: 14 it includes whitespaces
fee: number
engraving price, ex: 5000 ($50.00)
partNumber: string
variant identifier (used to add to cart), ex: 00082002599562_6565f4e5ddec8ce19105d26
retailerId: string
retailer identifier (used to map the Array<Retailer>), ex: 6565f4e8700628ce19106b14
price: number
variant price, ex: 1499
isEngravable: boolean
whether this retailer's variant allows engraving, ex: true
modalities: string[]
the methods of fulfillment available for the variant, ex: [ 'shipping', 'onDemand' ]
shipping: string
retailer's shipping fulfillment Id, * ONLY SHIPPING RETAILERS CAN HANDLE ENGRAVING ORDER *
onDemand: string
retailer's onDemand fulfillment Id
backOfBottle: string
url for back of bottle image
frontOfBottle: string
url for front of bottle image
lifestyle: Array<string>
array of lifestyle image urls
image: string
award image url
statement: string
award statement text
title: string
award title
image: string
recipe image url
ingredients: Array<AttributesRecipeIngredient>
array of recipe ingredients
steps: Array<string>
array of recipe steps
title: string
recipe title
name: string
ingredient name
amount: string
amount needed for recipe
link: string
video url
image: string
video thumbnail image url
title: string
video title
statement: string
tasting note description
image: string
associated image url
title: string
tasting note title
type: string
type of personalization
engravingMaxLines: number
maximum number of lines for engraving
engravingMaxCharsPerLine: number
maximum characters per line for engraving
location: Array<string>
available locations for personalization
width: number
width for personalization area
currency: string
the currency code for the prices
minimum: number
the lowest price available for this product across all retailers
average: number
the average price of the product across all retailers
maximum: number
the highest price available for this product across all retailers
containerType: string
awards: Array<>
location: string
salePrice: number
height: number
page
integer
Page number for pagination (starts at 0)
perPage
integer
Number of results to return per page
visitorId
string
Unique identifier for the visitor/user to enable personalized results
retailers
array<string>
Array of retailer identifiers
orderBy
string
Field to sort results by. Possible values: "price", "name", "brand", "price")
orderDirection
string
Sort direction. Possible values: "asc", "desc"
filters
array<>
Array of filter objects to refine search results
loc
Location object for determining real-time availability
isLegacy
boolean
Whether to return legacy identifiers
isLean
boolean
Whether to return a minimal response format
refresh
boolean
When set to true, a new access token will be generated and returned
navigation
Navigation schema
products
array<>
Array of matched products
retailers
array<>
Array of available retailers
Content-Type
application/json
Authorization
Bearer <YOUR_ACCESS_TOKEN>
search
string
Text to search for in the catalog. Can include product names, brands, or other attributes
pageToken
string
Token for pagination, used to retrieve the next or previous page of results
entity
string
Entity identifier for personalized results
statusCode
number
HTTP status code of the response
message
string
A brief message describing the result of the API call
metadata
Contains metadata about the API call
auth
Authentication object, only when refresh in true
languages
Array<string>
List of supported languages for the response, e.g. ["en"]
timestamp
string
Unix timestamp (in milliseconds) when the response was generated
timezone
string
Timezone used for the response, always "UTC"
requestId
string
{
"id": "50726f643247373737343330",
"name": "TITO'S HANDMADE VODKA",
"salsifyGrouping": "GROUPING-33277",
"brand": "TITO'S",
"catPath": "Spirits > Vodka > Regular Vodka",
"category": "Spirits",
"classification": "Vodka",
"type": "Regular Vodka",
"subType": "",
"region": "TEXAS",
"country": "UNITED STATES",
"material": "GRAIN",
"color": "WHITE",
"flavor": "",
"variety": "CORN",
"appellation": "TEXAS",
"vintage": "",
"description": "Tito's Handmade Vodka is America's Original Craft Vodka produced in Austin, Texas. Tito's is known for its high-quality product, charitable giving, and goal to make people happy while making the world a better place. Tito's is crafted in old-fashioned pot stills, with each batch taste-tested. Exceptionally smooth with an impeccably clean finish, our unflavored, low-calorie vodka is six times distilled, made from corn, and naturally gluten-free with no carbs or sugar*. From a Bloody Mary at brunch, a Transfusion on the golf course, or a simple Tito's and soda with friends, make every occasion a celebration with Tito's! Tito's Handmade Vodka turns spirits into love and goodness with Love, Tito's, the philanthropic heart of the company, supporting thousands of nonprofit organizations across the U.S. — and around the world — amplifying their missions of disaster relief and response, community building, animal welfare, veteran services, and so much more. Available in Liter, 1.75L, 750mL, 375mL, 200mL, and 50mL sizes. For more information, visit titosvodka.com.\n\n*(Average Analysis per 1.5 oz Tito's Handmade Vodka: 98 calories, Carbohydrates 0 grams, Protein 0 grams, Fat 0 grams, Sugar 0 grams)\n\nMADE IN BATCHES: Tito's Handmade Vodka is made in Austin, Texas, and distilled in old-fashioned pot stills, with each batch taste-tested.\n\nMADE FROM CORN: Our unflavored vodka is distilled from corn, making it naturally gluten-free. Tito's is 98 calories per 1.5 oz serving, and 65 calories per 1 oz serving.\n\nMIXES WITH EVERYTHING: Tito's mixes with everything, from a classic Tito's Soda Lime to the iconic Bloody Mary to a refreshing Tito's Transfusion, the official drink of the green.\n\nSPREAD THE LOVE: Tito's Handmade Vodka is dedicated to giving back through its Love, Tito's®️ and Vodka for Dog People®️ programs, plus a no-cost farmers market for employees at its Fourteen Acres™️ Farm.",
"htmlDescription": "<ul><li><p>Tito's Handmade Vodka is America's</p></li><li><p>Original Craft Vodka produced in Austin,</p></li><li><p>Texas. Tito's is known for its high-quality product, charitable giving, and goal to make people happy while making the world a better place. Tito's is crafted in old-fashioned pot stills, with each batch taste-tested. Exceptionally smoot</p></li><li><p>h with an impeccably clean finish, our unflavored, low-calorie vodka is six times distilled, made from corn, and naturally gluten-free with no carbs or sugar*. From a Bloody Mary at brunch, a Transfusion on the golf course, or a simple Tito's and soda with friends, make every occasion a celebration with Tito's! Tito's Handmade Vodka turns spirits into love and goodness with Love, Tito's, the philanthropic heart of the company, supporting thousands of nonprofit organizations across the U.S. — and around the world — amplifying their missions of disaster relief and response, community building, animal welfare, veteran services, and so much more. Available in Liter, 1.75L, 750mL, 375mL, 200mL, and 50mL sizes. For more information, visit titosvodka.com.</p></li></ul><p>*(Average Analysis per 1.5 oz Tito's Handmade Vodka: 98 calories, Carbohydrates 0 grams, Protein 0 grams, Fat 0 grams, Sugar 0 grams)</p><p>MADE IN BATCHES: Tito's Handmade Vodka is made in Austin, Texas, and distilled in old-fashioned pot stills, with each batch taste-tested.</p><p>MADE FROM CORN: Our unflavored vodka is distilled from corn, making it naturally gluten-free. Tito's is 98 calories per 1.5 oz serving, and 65 calories per 1 oz serving.</p><p>MIXES WITH EVERYTHING: Tito's mixes with everything, from a classic Tito's Soda Lime to the iconic Bloody Mary to a refreshing Tito's Transfusion, the official drink of the green.</p><p>SPREAD THE LOVE: Tito's Handmade Vodka is dedicated to giving back through its Love, Tito's®️ and Vodka for Dog People®️ programs, plus a no-cost farmers market for employees at its Fourteen Acres™️ Farm.</p>",
"tastingNotes": "Exceptionally Smooth. Distilled from corn, Tito's offers a taste that leads with smoky notes of roasted grain and black pepper and finishes with a subtle sweetness. Our gluten-free vodka is crafted for spirit connoisseurs and taste-tested to make sure you get the best juice we have to offer.",
"images": [
"https://assets.liquidcommerce.co/catalog/img/3d972802-7f88-410a-a6d7-15e3267f569a.png",
"https://assets.liquidcommerce.co/catalog/images/2023_Evergreen_LoveTitos_3000x3000.jpg",
"https://assets.liquidcommerce.co/catalog/images/2023_Evergreen_GoesDownSmooth_3000x3000.jpg",
"https://assets.liquidcommerce.co/catalog/images/2023_Evergreen_Corn_3000x3000.jpg",
"https://assets.liquidcommerce.co/catalog/images/image_-_2024-12-18T181404.457_(1).png",
"https://assets.liquidcommerce.co/catalog/images/(Image_4)_Evergreen_AustinTexas_3000x3000.png",
"https://assets.liquidcommerce.co/catalog/images/(Image_5)_Evergreen_Cocktails_3000x3000.png"
],
"abv": "40",
"proof": "80",
"age": "",
"availableFrom": null,
"sizes": [
{
"id": "6554c9a46b741e7ad37bc5db",
"upc": "00619947000020",
"size": "750 ML",
"volume": "750",
"catPath": "SPIRITS > VODKA > REGULAR VODKA",
"uom": "MILLILITRE",
"container": "Bottle",
"containerType": "Glass",
"pack": false,
"packDesc": "",
"image": "https://assets.liquidcommerce.co/catalog/img/3d972802-7f88-410a-a6d7-15e3267f569a.png",
"attributes": {
"presale": {
"canPurchaseOn": null,
"estimatedShipBy": null,
"language": "",
"isActive": false
},
"engraving": {
"maxCharsPerLine": 16,
"maxLines": 2,
"fee": 5000,
"status": true,
"location": "Above the Label, Front of the Bottle"
},
"maxQuantityPerOrder": 12
},
"variants": [
{
"partNumber": "00619947000020_retailerId-123",
"retailerId": "retailerId_123",
"modalities": [
"shipping"
],
"price": 1799,
"salePrice": 0,
"stock": 1878,
"isEngravable": true,
"fulfillmentTypes": {
"shipping": "fulfillmentId_123",
"onDemand": ""
},
"fulfillments": [
"fulfillmentId_123"
]
}
],
"salsifyPid": "926",
"maxQuantityPerOrder": 12
}
],
"attributes": {
"brandOrigin": "Tito's Handmade Vodka was founded by sixth-generation Texan, Tito Beveridge. His career track took a few turns, but he always found time to do what he loved: make people smile with spirits. Tito got a kick out of infusing vodka for friends and handing out bottles at parties. He quickly became known as \"the vodka guy.\" Willing to risk it all, he pursued the career of his dreams and set out to build a distillery. With a dog by his side, Tito found a plot of land in rural Austin, convinced a few friends to help him build a shack for his still and, against all odds, perfected his pot-distilled spirit. He fought Texas laws, took on nearly $90,000 of debt, and for several years, played a one-man show to get his dream going, overseeing all distilling, hand-bottling, and selling during the hours in between until he sold his first case. Over twenty-five years later, Tito's Handmade Vodka has remained true to its roots and is celebrated for its high-quality product, charitable contributions, and its goal to make people happy while making the world a better place.",
"originStatement": "Tito's Handmade Vodka was founded in 1997 by sixth-generation Texan, Tito Beveridge. Twenty-five years later, Tito's has remained true to its roots and is celebrated for its high-quality product, charitable contributions, and goal to make people happy while making the world a better place.",
"ownershipType": [],
"tags": [],
"images": {
"backOfBottle": "",
"frontOfBottle": "",
"lifestyle": [
"https://assets.liquidcommerce.co/catalog/images/2023_Evergreen_LoveTitos_3000x3000.jpg",
"https://assets.liquidcommerce.co/catalog/images/2023_Evergreen_GoesDownSmooth_3000x3000.jpg",
"https://assets.liquidcommerce.co/catalog/images/2023_Evergreen_Corn_3000x3000.jpg",
"https://assets.liquidcommerce.co/catalog/images/image_-_2024-12-18T181404.457_(1).png",
"https://assets.liquidcommerce.co/catalog/images/(Image_4)_Evergreen_AustinTexas_3000x3000.png",
"https://assets.liquidcommerce.co/catalog/images/(Image_5)_Evergreen_Cocktails_3000x3000.png"
]
},
"recipes": [
{
"image": "",
"ingredients": [
{
"name": "Tito's Handmade Vodka",
"amount": "1.5 oz"
},
{
"name": "Ginger Beer",
"amount": "3 oz"
},
{
"name": "Fresh Lime Juice",
"amount": "0.5 oz"
},
{
"name": "Lime Slice",
"amount": "Garnish"
}
],
"steps": [
"Add all ingredients to a Tito's Copper Mug with ice.",
"Stir and garnish with a lime slice."
],
"title": "TITO'S AMERICAN MULE"
}
],
"video": [
{
"link": "goOVywpBvPA",
"image": "",
"title": "The Story of Tito's Handmade Vodka"
}
],
"tastingNotes": [],
"awards": []
},
"priceInfo": {
"currency": "USD",
"minimum": 1699,
"average": 2320,
"maximum": 3247
}
}curl --location 'https://staging.api.liquidcommerce.cloud/catalog/search' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"search": "",
"pageToken": "",
"entity": "",
"page": 0,
"perPage": 10,
"orderBy": "price",
"orderDirection": "desc",
"filters": [
{
"key": "categories",
"values": [
"SPIRITS > TEQUILA"
]
},
{
"key": "engraving",
"values": "YES"
},
{
"key": "fulfillment",
"values": [
"onDemand"
]
},
{
"key": "tags",
"values": [
"WOMAN OWNED"
]
}
],
"loc": {
"address": {
"one": "100 madison ave",
"two": "apt 1707",
"city": "New york",
"state": "NY",
"zip": "10016"
}
},
"isLegacy": true,
"isLean": false,
"refresh": false
}
'{
"statusCode": 200,
"message": "Liquid Catalog, unique AI driven product discovery and recommendations.",
"metadata": {
"languages": ["en"],
"timestamp": 1731603976867,
"timezone": "UTC",
"requestId": "requestid_123abc456def",
"path": "/api/catalog/search",
"version": "1.7.0"
},
"navigation": {
"correctedQuery": "",
"attributionToken": "attr123",
"currentPage": 0,
"totalPages": 1,
"totalCount": 2,
"availableOrderBy": ["price"],
"availableOrderDirection": ["asc", "desc"],
"cursor": {
"nextPageToken": "next123",
"previousPageToken": "prev123"
},
"filters": [
{
"bucket": "brands",
"values": [
{
"value": "CASA DRAGONES",
"count": 2
}
]
}
]
},
"products": [
{
"id": "30d851c7dab529f303f4931b",
"salsifyGrouping": "GROUPING-1445173",
"name": "TEQUILA CASA DRAGONES AÑEJO BARREL BLEND",
"brand": "CASA DRAGONES",
"catPath": "Spirits > Tequila > Añejo",
"category": "",
"classification": "",
"type": "",
"subType": "",
"region": "Jalisco",
"country": "Mexico",
"material": "Agave",
"color": "White",
"flavor": "",
"variety": "",
"appellation": "",
"abv": "40",
"proof": "80",
"age": "2 Year",
"vintage": "",
"description": "Casa Dragones Barrel Blend, 100% Blue Agave Añejo tequila, achieves its distinctive character from being matured in two different wood barrels, new French Oak and new American Oak, each selected for their individual flavor and characteristics. At the end of the aging process, both barrel styles are blended together to create a uniquely smooth, agave-forward taste profile.",
"htmlDescription": "<p>Casa Dragones Barrel Blend, 100% Blue Agave Añejo tequila, achieves its distinctive character from being matured in two different wood barrels, new French Oak and new American Oak, each selected for their individual flavor and characteristics. At the end of the aging process, both barrel styles are blended together to create a uniquely smooth, agave-forward taste profile.</p>",
"tastingNotes": ""A subtle melange of flavors." - Wine Spectator",
"images": [
"https://example.com/images/product1.jpg"
],
"sizes": [
{
"id": "6554c9a46b741e7ad37c8ad2",
"upc": "00850005002161",
"size": "750 ML",
"volume": "750",
"uom": "MILLILITRE",
"container": "Bottle",
"containerType": "",
"pack": false,
"packDesc": "",
"image": "https://example.com/images/bottle1.jpg",
"attributes": {
"engraving": {
"status": true,
"maxLines": 3,
"maxCharsPerLine": 16,
"fee": 5000,
"location": "Between the Labels"
}
},
"variants": [
{
"partNumber": "00850005002161_6565f4e8700628ce19106bc7",
"retailerId": "6565f4e8700628ce19106bc7",
"modalities": [
"shipping"
],
"price": 14999,
"salePrice": 0,
"stock": 100,
"isEngravable": false,
"fulfillmentTypes": {
"shipping": "65830af0be8824843febdb8c",
"onDemand": ""
},
"fulfillments": [
"65830af0be8824843febdb8c"
]
}
],
"salsifyPid": "2216260"
}
],
"attributes": {
"brandOrigin": "",
"originStatement": "The wood used for their new French Oak barrels is a proprietary blend of 100% Quercus Sessile Oak, sustainably sourced from five different forests in the centre of France, then custom toasted to impart roundness and light spicy notes, while respecting the minerality of the soil. The new American Oak barrels receive a custom medium toast, imparting the complexity of the agave and a beautiful aromatic intensity.",
"ownershipType": [
"Woman Owned"
],
"tags": [
"Woman Owned"
],
"images": {
"backOfBottle": "",
"frontOfBottle": "",
"lifestyle": []
},
"awards": [],
"recipes": [
{
"image": "",
"ingredients": [
{
"name": "Casa Dragones Añejo Barrel Blend",
"amount": "2 oz."
},
{
"name": "Vanilla Agave Syrup",
"amount": "1/4 oz."
},
{
"name": "Bittermens Mole Bitters",
"amount": "2 dashes"
}
],
"steps": [
"Stir with ice and strain into a chilled rocks glass with one large ice cube.",
"Garnish with a long mandarin orange twist."
],
"title": "El Grito"
}
],
"video": [],
"tastingNotes": [
{
"statement": "Light caramel, with bright hues and pronounced legs.",
"image": "https://example.com/images/tasting1.png",
"title": "Body & Color"
},
{
"statement": "Fresh floral, pear with notes of figs and almonds.",
"image": "https://example.com/images/tasting2.png",
"title": "Aroma"
},
{
"statement": "Notes of macadamia, nutmeg and blackberry.",
"image": "https://example.com/images/tasting3.png",
"title": "Taste"
},
{
"statement": "Long round finish, notes of cacao, spicy black pepper.",
"image": "https://example.com/images/tasting4.png",
"title": "Finish"
}
]
}
},
{
"id": "66ae3111e2e91f45afac28f7",
"salsifyGrouping": "GROUPING-330410",
"name": "Tequila Casa Dragones Reposado Mizunara",
"brand": "Casa Dragones",
"catPath": "Spirits > Tequila > Reposado",
"region": "Jalisco",
"country": "Mexico",
"material": "Agave",
"color": "Tawny/brown",
"variety": "100% Blue Agave",
"appellation": "Jalisco",
"abv": "40",
"proof": "80",
"age": "1 Year",
"description": "Casa Dragones Reposado Mizunara, 100% Blue Agave Reposado tequila; is the first tequila rested exclusively in new Mizunara casks, a rare oak native to Japan, traditionally used for aging Japanese whiskies.",
"images": [
"https://example.com/images/product2.jpg"
],
"sizes": [
{
"id": "4efe5a4554660ad190c99d1c",
"upc": "00850005002215",
"size": "750 ML",
"volume": "750",
"uom": "MILLILITRE",
"container": "Bottle",
"image": "https://example.com/images/size2.jpg",
"attributes": {
"engraving": {
"status": true,
"maxLines": 3,
"maxCharsPerLine": 16,
"fee": 5000,
"location": "Between the Labels"
}
},
"variants": [
{
"partNumber": "8222419_b8fd6f32e6a47930f717c111",
"retailerId": "c2c8a24ee2b876280e5e0fa3",
"modalities": ["onDemand"],
"price": 15999,
"salePrice": 0,
"stock": 100,
"isEngravable": false,
"fulfillments": ["26978cee5037ead268f3c0cc"]
}
]
}
],
"attributes": {
"tastingNotes": [
{
"statement": "Light, bright golden with a silky texture, and long pronounced legs.",
"image": "https://example.com/images/tasting3.png",
"title": "Body & Color"
},
{
"statement": "Orange blossom, magnolia with gentle notes of honey and sandalwood.",
"image": "https://example.com/images/tasting4.png",
"title": "Aroma"
}
]
}
}
],
"retailers": [
{
"id": "ret_abc123xyz789",
"name": "LiquidCommerce Wine & Spirits",
"platformFee": 499,
"address": {
"one": "240 Loisaida Avenue",
"two": "",
"city": "New York",
"state": "NY",
"zip": "10009",
"country": "US"
},
"fulfillments": [
{
"id": "ful_ghi189rst123",
"timezone": "America/New_York",
"type": "onDemand",
"canEngrave": false,
"fees": {
"min": 1999,
"fee": 0,
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Arrives in 60 mins",
"short": "60 mins"
},
"hours": {
"monday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"tuesday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"wednesday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"thursday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"friday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"saturday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
},
"sunday": {
"active": true,
"times": [
{
"startsAt": "10:00",
"endsAt": "21:00"
}
]
}
},
"breaks": [],
"items": []
}
]
},
{
"id": "ret_abc123xyz189",
"name": "LiquidCommerce Barn",
"platformFee": 499,
"address": {
"one": "1000 N 5th Ave",
"two": "134",
"city": "Vernon Hills",
"state": "NY",
"zip": "10061",
"country": "US"
},
"fulfillments": [
{
"id": "ful_abc123xyz189",
"timezone": "America/Chicago",
"type": "shipping",
"canEngrave": true,
"fees": {
"pack": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"individual": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Ships in 2-3 days",
"short": "2-3 days"
},
"hours": {
"monday": {
"active": false,
"times": []
},
"tuesday": {
"active": false,
"times": []
},
"wednesday": {
"active": false,
"times": []
},
"thursday": {
"active": false,
"times": []
},
"friday": {
"active": false,
"times": []
},
"saturday": {
"active": false,
"times": []
},
"sunday": {
"active": false,
"times": []
}
},
"breaks": [],
"items": []
}
]
}
]
}Unique identifier for the API request. Used for debugging and support
path
string
API path
version
string
API version used for the request
A full reference to a cart type for different Liquid Commerce APIs.
id: string
unique cart identifier
quantity: number
cartoptional Salsify grouping identifier
salsifyPid: string [optional]
optional Salsify product identifier
partNumber: string
retailer's part number
upc: string
universal product code
name: string
product name
brand: string
product brand
size: string
product size
volume: string
product volume
uom: string
unit of measure
catPath: string
category path
abv: string
alcohol by volume, ex: 40 (40%)
proof: string
alcohol proof
container: string
product container, ex: Bottle
containerType: string
product container type, ex: Glass
customerPlacement:
Indicates fulfillment type
pack: boolean
whether the product is a pack, ex: true
packDesc: string
if the product is a pack and a description is available, ex: 8pk
quantity: number
quantity of item
maxQuantity: number
maximum allowed quantity
unitPrice: number
price per unit
price: number
total price for quantity
scheduleFor: string | Date
scheduled delivery date/time
availableAt: string | Date
availability date/time
images: Array<string>
Product images
mainImage: string
primary product image
attributes:
item-specific attributes
retailer's engraving fee
subtotal: number
Current subtotal for retailer items
total: number
Total amount including all fees and taxes
address:
location information split out to individual values for the street, city, state, zip code and country
fulfillments: Array<>
retailer available fulfillment configurations
NoItemsInCart
InvalidId
NoId
CartCheckoutProcessed
NewCart
CartError
ItemQuantityChange
ItemIdNotFound
ItemsRemoved
RetailerFulfillmentInvalid
MaxQuantityPerOrderExceeded
RetailerOnDemandHoursNotAvailable
CouponProcessingError
CouponNotFound
CouponExpired
NoApplicableDiscount
CouponNotStarted
MinimumOrderValueNotMet
MinimumOrderUnitsNotMet
MinimumDistinctItemsNotMet
QuotaExceeded
UserLimitExceeded
NotFirstPurchase
InvalidCoupon
InvalidMembership
InvalidDomain
InvalidRequirements
InvalidOrganization
PresaleItemsNotAllowed
ProductNotEligible
NotEnoughPreviousOrders
RetailerDoesNotAllowPromos
RetailerDoNotAllowPromos
RetailerDoesNotAllowGiftCards
RetailerDoNotAllowGiftCards
engraving placement
lines: Array<string>
engraving text content
total number of items in cart
platformFee: number
platform fee for the cart
deliveryFee: number
total delivery fees
engravingFee: number
total engraving fees
shippingFee: number
total shipping fees
discounts: number
total discount fees
giftCardTotal: number
total value of applied gift cards
subtotal: number
cart subtotal before fees/discounts
total: number
final cart total
isPresaleLocked: boolean
indicates if the cart is locked to a single presale item
presaleExpiresAt: Date | null
the timestamp when the presale lock expires
createdAt: Date
cart creation timestamp
updatedAt: Date
last update timestamp
items: Array<CartItem>
array of cart items
loc: LocType
location information
retailers: Array<CartRetailer>
array of retailers for items
attributes: CartAttributes
cart-level attributes
events: Array<CartEvent>
cart-related events, Cart Events
id: string
identifier of the item
retailerId: string
identifier of retailer providing item
fulfillmentId: string
identifier of fulfillment method
variantId: string
product variant identifier
liquidId: string
internal LiquidCommerce product identifier
standard: string
Item in stock and ready to ship
back_order: string
Item out of stock, fulfillment upon restock
pre_sale: string
Item can be purchased now, ships on release date
id: string
retailer identifier
name: string
retailers name, ex: Liquid Wine and Spirits
platformFee: number
platform fee for retailer
deliveryFee: number
retailer's delivery fee
shippingFee: number
retailer's shipping fee
promoCode: CartAttributesPromoCode
applied promotional code
amounts: CartAttributesAmounts
cart amount calculations
type: CartEventTypes
type of cart event
message: string
event description
items: Array<Partial<CartItem>>
affected items if applicable
OutOfStock
PresaleLimitExceeded
PresaleNotStarted
PresaleExpired
PresaleMixedCart
ItemsRequestedNotAdded
ItemEngravingError
AddressChange
RemovedExistingCartItems
RetailerMinNotMet
value: string
the promotional code value
discount: number
the discount promo code number
freeDelivery: boolean
whether promo provides free delivery
freeServiceFee: boolean
whether promo waives service fees
freeShipping: boolean
whether promo provides free shipping
fee calculations
discounts: CartAttributesAmountsDiscounts
Discount calculations
shipping: number
total shipping fees
onDemand: number
total on-demand delivery fees
shipping: number
shipping discounts
onDemand: number
on-demand delivery discounts
engraving: number
engraving discounts
service: number
service fee discounts
products: number
product discounts
engraving: CartItemEngraving
Engraving options/configuration
giftCard: CartItemGiftCart
Gift card options
presale: ProductPresale
Representing a product presale
isEngravable: boolean
whether item can be engraved
hasEngraving: boolean
whether engraving is applied
fee: boolean
engraving service fee
maxCharsPerLine: number
maximum characters per line
maxLines: number
maximum number of lines
sender: string
the sender of the gift
message: string
the message attached to the gift
recipients: Array<string>
the list of recipients of the gift
sendDate: string
the date when the gift should be sent
salsifyGrouping: string [optional]
engravingFee: number
location: string
{
"id": "cart_abc123xyz789",
"quantity": 1,
"platformFee": 499,
"deliveryFee": 0,
"shippingFee": 1599,
"engravingFee": 0,
"discounts": 0,
"giftCardTotal": 0,
"subtotal": 5999,
"total": 8097,
"createdAt": "2024-11-14T19:00:45.433Z",
"updatedAt": "2024-11-14T19:00:45.433Z",
"items": [
{
"id": "item_abc123xyz789",
"variantId": "var_abc123xyz789",
"liquidId": "liq_abc123xyz789",
"retailerId": "ret_abc123xyz789",
"partNumber": "pn_abc123xyz789",
"fulfillmentId": "ful_abc123xyz789",
"upc": "88320002003",
"catPath": "WINE > ROSE WINE",
"volume": "1.5",
"uom": "LITRE",
"pack": false,
"packDesc": "",
"container": "Bottle",
"containerType": "Bottle",
"name": "Whispering Angel Rosé",
"brand": "Chateau D'esclans",
"scheduledFor": "",
"abv": "14",
"proof": "28",
"size": "1.5 L",
"price": 5999,
"quantity": 1,
"customerPlacement": "standard",
"maxQuantity": 12,
"unitPrice": 5999,
"availableAt": "",
"mainImage": "https://storage.example.com/images/products/image1.png",
"images": [
"https://storage.example.com/images/products/image1.png",
"https://storage.example.com/images/products/image2.png",
"https://storage.example.com/images/products/image3.png",
"https://storage.example.com/images/products/image4.png",
"https://storage.example.com/images/products/image5.png"
],
"attributes": {
"giftCard": {
"sender": "",
"message": "",
"recipients": [],
"sendDate": ""
},
"engraving": {
"isEngravable": true,
"hasEngraving": false,
"maxCharsPerLine": 0,
"maxLines": 0,
"fee": 0,
"location": "",
"lines": []
}
}
}
],
"loc": {
"coords": {
"lat": 40.7447986,
"long": -73.98530819999999
},
"address": {
"one": "100 madison ave",
"two": "apt 1707",
"city": "New york",
"state": "NY",
"zip": "10016",
"country": "US",
"placesId": "place_abc123xyz789",
"customerId": "cust_abc123xyz789",
"type": "shipping",
"lat": 40.7447986,
"long": -73.98530819999999
}
},
"retailers": [
{
"id": "ret_abc123xyz789",
"name": "East Houston St Wine & Liquors",
"platformFee": 499,
"address": {
"one": "250 E Houston Street",
"two": "",
"city": "New York",
"state": "NY",
"zip": "10002",
"country": "US"
},
"fulfillments": [
{
"id": "ful_abc123xyz789",
"timezone": "America/New_York",
"type": "shipping",
"canEngrave": false,
"fees": {
"pack": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"individual": {
"active": true,
"maxQuantity": 25,
"fee": 1599
},
"free": {
"active": false,
"min": 0
}
},
"expectation": {
"detail": "Ships in 2-3 days",
"short": "2-3 days"
},
"hours": {
"monday": {
"active": false,
"times": []
},
"tuesday": {
"active": false,
"times": []
},
"wednesday": {
"active": false,
"times": []
},
"thursday": {
"active": false,
"times": []
},
"friday": {
"active": false,
"times": []
},
"saturday": {
"active": false,
"times": []
},
"sunday": {
"active": false,
"times": []
}
},
"breaks": [],
"items": [
"item_abc123xyz789"
],
"engravingFee": 0,
"shippingFee": 1599,
"deliveryFee": 0,
"subtotal": 5999
}
],
"engravingFee": 0,
"deliveryFee": 0,
"shippingFee": 1599,
"subtotal": 5999,
"total": 8097
}
],
"attributes": {
"promoCode": {
"value": "",
"freeDelivery": false,
"freeServiceFee": false,
"freeShipping": false
},
"giftCards": [],
"amounts": {
"fees": {
"shipping": 5999,
"onDemand": 0
},
"discounts": {
"shipping": 0,
"onDemand": 0,
"engraving": 0,
"service": 0,
"products": 0
}
}
},
"events": [
{
"type": "NoId",
"message": "The cartId provided is empty, a new cart was created",
"items": []
},
{
"type": "PartnerProductConfigs",
"message": "Item(s) have been removed, they're currently not available in your account's catalog. Check your LiquidCommerce Partner App account and add the product to you account, or just use our default catalog",
"items": [
{
"partNumber": "pn_abc123xyz789",
"quantity": 1,
"fulfillmentId": "ful_abc123xyz789",
"id": "item_abc123xyz789"
}
]
}
]
}A full reference to a checkout type for the Liquid Commerce Prepare API.
token: string
Unique identifier for the checkout session
cartId: string
Checkout retailer includes accumulated totals using all the same properties from the type for all their available fulfillments. Below are all the values additional to the amounts values.
Checkout retailer fulfillments includes accumulated totals using all the same properties from the type for the fulfillments. Below are all the values additional to the amounts values.
checkoutcheckoutCustomer's phone
birthDate: string
Customer's birthDate
profileImage: string
Customer's profile image
hasAgeVerify: boolean (moved to level)
Should the checkout verify age
createdAt: Date
Customer's creation time
updatedAt: Date
Customer's profile updated time
Billing address apt, suite, floor, etc
city: string
Billing address city
state: string
Billing address state, 2 letter code
zip: string
Billing address zip code
country: string
Billing address country
Billing address's recipient phone
one: string
Billing address street address
two: string
Billing address apt, suite, floor, etc
city: string
Billing address city
state: string
Billing address state, 2 letter code
zip: string
Billing address zip code
country: string
Billing address country
Amounts total platform
discounts: number
Amounts total discounts
giftCards: number
Amounts total giftCards
tax: number
Amounts total tax
tip: number
Amounts total tip
total: number
Amounts total
details:
Total amounts details, tax and discount breakdown
Amounts total product sales tax fee
Unit of measure
mainImage: string
Main product image URL
unitTax: number
Tax per unit
partNumber: string
Product part number
fulfillmentId: string
Item's fulfillment id
cartItemId: string
Unique cart item ID
pack: boolean
If the product is a pack, then true
packDesc: string
Product pack description
container: string
Product container, ex: Bottle
containerType: string
Product container type, ex: Glass
customerPlacement:
Indicates fulfillment type
name: string
Product name
brand: string
Product brand
upc: string
Product upc
abv: string
Product's alcohol by volume
proof: string
Product's alcohol proof
volume: string
Product's volume
size: string
Product size
catPath: string
Product category path based on the
quantity: number
Item's quantity
unitPrice: number
Individual item price
price: number
Total item price according to the quantity added
bottleDeposits: number
Total item's bottle deposit tax fee
tax: number
Total item's sales tax
attributes:
Item configuration attributes
fee of the engraving
lines: string[]
engraving message lines, ex:
Amounts total shipping
delivery: number
Amounts total delivery
discounts: number
Amounts total discounts
platform : number
Amounts total platform
giftCards: number
Amounts total gifts card
tax: number
Amounts total tax
tip: number
Amounts total tip
total: number
Amounts total
details:
Total amounts details, tax and discount breakdown
address:
Retailers address
fulfillments: Array<>
Retailers fulfillment methods
Fulfillment item ids
doesAllowPromos: boolean
indicates whether the retailer per fulfillment level support promo codes for cart & checkout
doesAllowGiftCards: boolean
indicates whether the retailer per fulfillment level support gift cards for checkout
Associated cart identifier
customer: Customer
Customer information
hasAgeVerify: boolean
Should the checkout verify age
hasSubstitutionPolicy: boolean
Should the checkout allow for substitutions
isGift: boolean
When the order is set as gift (isGift is set to true), the system adds gift messaging, special packaging, and gift receipts with optional sender anonymity.
createdAt: dateString
Checkout date of creation
updatedAt: dateString
Checkout last updated at date
isPresaleLocked: boolean
Indicates if the checkout is locked to a single presale item
presaleExpiresAt: Date | null
billingSameAsShipping: boolean
Checkout billing address same as shipping address
acceptedAccountCreation: boolean
Whether customer accepted account creation
giftOptions: GiftOptions
Checkout gift options
marketingPreferences: MarketingPreferences
Checkout marketing opt-ins
shippingAddress: Address
Checkout shipping address info, (this is address is propagated from the address set in the cart, if you need to update it you must update the cart. Address changes might produce price changes)
billingAddress: BillingAddress | Checkout BillingAddress
Checkout billing address info. BillingAddress information supporting both new and legacy formats. Recommended to use Checkout BillingAddress for new implementations
amounts: CheckoutAmounts
Checkout total amounts info
items: Array<CheckoutItem>
All checkout items
retailers: Array<CheckoutRetailer>
All checkout retailers
payment: string [optional]
Payment method identifier
giftCards:Array<CheckoutGiftCard>
Gift card codes to apply as payment methods
events:Array<CheckoutEvents>
Events related to the checkout process
promoCode: CheckoutPromoCode
Promotional code infos to apply a discount to the checkout
id: string
Customer's identifier
firstName: string
Customer's first name
lastName: string
Customer's last name
email: string
Customer's email
company:string
Customer's company
message: string
Checkout gift order message, max length: 500 characters
recipient: GiftRecipient
Checkout gift order recipient
name: string
Checkout gift order recipient name
email: string
Checkout gift order recipient email
phone: string
Checkout gift order recipient phone
canEmail: boolean
Checkout marketing opt-in for email
canSms: boolean
Checkout marketing opt-in for sms
fulfillmentId: boolean
Identifier of fulfillment
tip: number
The value of the tip
code: string
The unique code of the gift card used for identification
applied: number
The amount deducted from the gift card during the current transaction
balance: number
The remaining balance on the gift card after the applied amount is deducted
type: string
The type of the checkout event
message: string
A message providing additional context or details about the event
items: Array<Partial<CheckoutItem>> [optional]
Array of products details affecting the event
firstName: string
Billing address's recipient first name
lastName: string
Billing address's recipient last name
email: string
Billing address's recipient email
phone: string
Billing address's recipient phone
one: string
Billing address street address
id: string
Billing address's recipient identifier
firstName: string
Billing address's recipient first name
lastName: string
Billing address's recipient last name
email: string
Billing address's recipient email
company: string
Billing address's recipient company
subtotal: number
Amounts total subtotal
engraving: number
Amounts total engraving
service: number
Amounts total service
shipping: number
Amounts total shipping
delivery: number
Amounts total delivery
taxes: AmountDetailsTaxes
Amounts total taxes breakdown
discounts: AmountDetailsDiscounts
Amounts total discounts breakdown
bag: number
Amounts total bag tax fee, ex: Checkout bag fees are required for certain states
bottleDeposits: number
Amounts total bottle deposit tax fee, ex: bottle deposit fees are required for certain states
retailDelivery: number
Amounts total retail delivery tax fee, only required for the state of Colorado (CO)
delivery: number
Amounts total delivery tax fee, for on demand delivery only
shipping: number
Amounts total shipping tax fee, for shipping based order only
engraving: number
Amounts total engraving fee discounts
delivery: number
Amounts total delivery fee discounts, for on demand delivery only
shipping: number
Amounts total shipping fee discounts, for shipping based order only
products: number
Amounts total product discounts
service: number
Amounts total service discounts
variantId: string
Unique retailer item ID for the checkout item
retailerId: string
Item's retailer id
liquidId: string
Internal product identifier
salsifyPid: string
Salsify product identifier
salsifyGrouping: string
Salsify grouping identifier
giftCard: ItemGiftCard
item giftcard configurations
engraving: ItemEngraving
item engraving configurations
presale: ItemPresale
item presale configurations
sender: string
name of the gift card sender
message: string
message from the gift card sender
recipients: string[]
recipients to receive the gift card
sendDate: date
date in which to send the gift card
isEngravable: boolean
true when the item allows engraving
hasEngraving: boolean
true when the item is engraved
maxCharsPerLine: number
maximum of characters per engraving line
maxLines: number
max of engraving lines
location: string
location of the engraving
canPurchaseOn: Date | null
the date when the product can be purchased
estimatedShipBy: Date | null
the date when the product is expected to ship
isActive: boolean
whether the presale is currently active
language: string
the language associated with the product presale
id: string
Retailers identifier
name: string
Retailers name
subtotal: number
Amounts total subtotal
engraving: number
Amounts total engraving
service: number
Amounts total service
id: string
Fulfillment identifier
scheduledFor: string | Date
If the fulfillment method is type: onDemand the scheduled date and time in an ISO date string
deliveryInstructions: string
Delivery instructions
type: string
Fulfillment type, "onDemand" | "shipping"
expectation: CheckoutFulfillmentExpectation
Fulfillment expectation configurations
detail: string
detailed expectation of fulfillment, ex: Ships in 3 days
short: string
short expectation of fulfillment, ex: 3 days
fulfillmentId: string
unique identifier for the delivery method
instructions: string
delivery guidelines for the selected method, max length: 250 characters
value: string
The promotional code value
discount: number
The discount promo code number
freeDelivery: boolean
Whether promo provides free delivery
freeServiceFee: boolean
Whether promo waives service fees
freeShipping: boolean
Whether promo provides free shipping
phone: string
two: string
phone: string
platform: number
products: number
uom: string
fee: number
shipping: number
items: string[]
A full reference to a order type for the Liquid Commerce Orders APIs.
The Order object represents the complete structure of an order in the LiquidCommerce system. It contains comprehensive information about the order including customer details, addresses, items, retailers, fulfillment details, and financial information.
Represents customer details associated with the order.
Extends the basic address with additional fields.
Represents various order preferences and settings.
Detailed financial information for the order.
Represents retailers associated with the order.
Extends the basic address with two properties for coordinates.
Represents order fulfillment details.
Represents individual products in the order.
Orders in the LiquidCommerce system typically progress through these statuses:
created - Initial state when an order is first created in the system
processing - Order is actively being prepared/fulfilled
inTransit - Order has been sent out for delivery to the destination
Special cases:
Orders can be canceled from created or processing statuses
{
"cartId": "cartId_123abc123abc",
"hasSubstitutionPolicy": true,
"isGift": true,
"customer": {
"id": "customerId_123abc123abc",
"firstName": "Jon",
"lastName": "Doe",
"email": "[email protected]",
"company": "ReserveBar",
"phone": "(601) 952-1325",
"birthDate": "2003-11-12"
},
"billingSameAsShipping": false,
"shippingAddressTwo": "Apartment 4",
"acceptedAccountCreation": true,
"marketingPreferences": {
"canEmail": true,
"canSms": true
},
"billingAddress": {
"firstName": "Jon",
"lastName": "Doe",
"email": "[email protected]",
"phone": "(601) 952-1311",
"company": "ReserveBar",
"one": "1600 Amphitheatre Parkway",
"two": "Apartment 1",
"city": "New york",
"state": "FL",
"zip": "10016"
},
"giftOptions": {
"message": "Cheers!",
"recipient": {
"name": "User",
"phone": "(601) 952-1325",
"email": "[email protected]"
}
},
"hasAgeVerify": false
}"lines": [
"Engraving line 1",
"Engraving line 2"
]phone
string | null
Customer phone
birthdate
string | null
Customer birthdate in YYYY-MM-DD format
zip
string
Zip code
country
string
Country
company
string | null
Billing company
allowsSubstitution
boolean
Does the customer allow product substitution
billingSameAsShipping
boolean
Is billing address same as shipping address
deliveryInstructions
string | null
Delivery instructions
marketingPreferences
Marketing preferences
engraving
number
Engraving cost
service
number
Service fee
delivery
number
Delivery fee
discounts
number
Total discounts
giftCards
number
Gift cards amount applied
tip
number
Tip amount
total
number
Order total
taxDetails
Tax details
discountDetails
Discount details
bottleDeposits
number
Bottle deposits tax
retailDelivery
number
Retail delivery tax
service
number
Service discounts
code
string | null
Gift card code
timezone
string
Retailer IANA timezone
address
Retailer address
amounts
Retailer amounts
fulfillments
Array<>
Retailer fulfillments
updatedAt
string
Last updated date in ISO format
itemIds
Array<string>
IDs of items in this fulfillment
cancellation
Cancellation information
expectationFormatted
Delivery expectation formatted
packages
Array<>
Packages for this fulfillment
timeline
Array<>
Fulfillment timeline
status
Package status
dateShipped
string | null
Date shipped in ISO format
liquidId
string | null
Liquid ID
legacyGrouping
string | null
Legacy grouping
legacyPid
string | null
Legacy product ID
customerPlacement
Customer placement type
isPresale
boolean
Is this a presale item
expectation
string | null
Formatted delivery expectation specific to the item
estimatedShipBy
string | null
Estimated ship by date for presale items
product
Product details
image
string | null
Product image URL
pricing
Item pricing details
attributes
Item attributes
mskus
Array<string>
Product SKUs
category
string | null
Product category
size
string | null
Product size
volume
string | null
Product volume
uom
string | null
Unit of measure
proof
string | null
Product proof
attributes
Product attributes
containerType
string | null
Container type
bottleDeposits
number
Bottle deposits amount
delivered - Order has been successfully delivered to the destinationcanceled - Order has been canceled (can occur at any stage except delivered)
referenceId
string | null
Optional order reference ID
legacyOrderNumber
string | null
Optional legacy order number for backward compatibility
isHybrid
boolean
Indicates if this is a hybrid order
partnerId
string
Partner ID associated with the order
partnerName
string
Partner name associated to the order
createdAt
string
Order creation date in ISO format
updatedAt
string
Order last update date in ISO format
customer
Customer information
addresses
Order addresses (shipping and billing)
options
Order options including gift details and preferences
paymentMethods
Array<PaymentMethods>
Order payment methods information
amounts
Order amount details including subtotal, taxes, fees, etc.
retailers
Array<OrderRetailer>
Order retailers information
items
Array<OrderItem>
Order items details
id
string
Customer ID
firstName
string | null
Customer first name
lastName
string | null
Customer last name
email
string
Customer email
one
string
Address line 1
two
string | null
Address line 2
city
string
City
state
string
State
firstName
string | null
Billing first name
lastName
string | null
Billing last name
email
string
Billing email
phone
string | null
shipping
Shipping address
billing
Billing address
isGift
boolean
Is this order a gift
giftMessage
string | null
Gift message
giftRecipient
Gift recipient information
hasVerifiedAge
boolean
Has the customer verified their age
name
string | null
Gift recipient name
email
string | null
Gift recipient email
phone
string | null
Gift recipient phone
email
boolean
Email marketing preference
sms
boolean
SMS marketing preference
subtotal
number
Order subtotal
shipping
number
Shipping cost
platform
number
Platform fee
tax
number
Tax amount
products
number
Product tax
shipping
number
Shipping tax
delivery
number
Delivery tax
bag
number
Bag tax
products
number
Product discounts
shipping
number
Shipping discounts
delivery
number
Delivery discounts
engraving
number
Engraving discounts
type
string | null
Payment type, ex: CREDIT_CARD, GIFT_CARD, etc
card
string | null
Card type
last4
string | null
Card last 4 digits
holder
string | null
Card holder
id
string
Retailer ID
legacyId
string | null
Retailer legacy ID
name
string
Retailer name
system
Order system type
latitude
number | null
Latitude coordinate
longitude
number | null
Longitude coordinate
id
string
Fulfillment ID
type
Fulfillment type
status
Fulfillment status
scheduledFor
string | null
Scheduled date in ISO format
category
string
Category of cancellation
subcategory
string | null
Subcategory of cancellation
notes
string | null
Specific note left for this cancellation
detail
string
Estimated delivery of a shipping or on-demand fulfillment
engraving
string | null
Estimated delivery for an engraving fulfillment
id
string
Package ID
carrier
string | null
Shipping carrier
trackingNumber
string | null
Tracking number
trackingUrl
string | null
Tracking URL
status
Fulfillment status
timestamp
string
Status timestamp in ISO format
id
string
Item ID
fulfillmentId
string | null
Fulfillment ID
retailerId
string
Retailer ID
variantId
string
Variant ID
name
string
Product name
brand
string
Product brand
upc
string
Product UPC
sku
string
Product SKU
pack
boolean
Is this a pack
packDescription
string | null
Pack description
abv
string | null
Alcohol by volume
container
string | null
Container
price
number
Item price
unitPrice
number
Unit price
quantity
number
Item quantity
tax
number
Item tax
engraving
Item engraving details
giftCard
Item gift card details
hasEngraving
boolean
Has engraving
fee
number
Engraving fee
location
string | null
Engraving location
lines
Array<string>
Engraving text lines
sender
string | null
Gift card sender
message
string | null
Gift card message
recipients
Array<string>
Gift card recipients
sendDate
string | null
Gift card send date
Billing phone
enum ENUM_ORDER_STATUS {
CREATED = 'created',
PROCESSING = 'processing',
IN_TRANSIT = 'inTransit',
CANCELED = 'canceled',
DELIVERED = 'delivered',
}enum ENUM_ORDER_SYSTEM {
LIQUIDCOMMERCE = 'LiquidCommerce OMS',
RESERVEBAR = 'ReserveBar OMS',
}enum ENUM_ORDER_FULFILLMENT_TYPE {
SHIPPING = 'shipping',
ON_DEMAND = 'onDemand',
DIGITAL = 'digital',
BOPIS = 'bopis'
}enum ENUM_ORDER_PACKAGE_STATUS {
PENDING = 'pending',
SHIPPED = 'shipped',
DELIVERED = 'delivered',
RETURNED = 'returned',
CANCELED = 'canceled'
}enum ENUM_CUSTOMER_PLACEMENT {
STANDARD = 'standard',
BACK_ORDER = 'back_order',
PRE_SALE = 'pre_sale'
}Technical details for partners integrating with the Event Bridge system to receive real-time order event notifications via webhooks. Our service processes orders and sends it to your provided endpoint
To integrate, our team will work with you to configure:
Your Webhook URL: A secure HTTPS URL endpoint on your system capable of receiving HTTP POST requests with a JSON payload.
Shared Secret (Generated by Us): We will generate a unique, secure secret string for your specific webhook configuration. This secret must be used by you to verify the HMAC signature on incoming requests. We will provide this secret to you securely.
Custom Headers (Optional): You can specify any additional HTTP headers (key-value pairs) you require us to include in the webhook requests (e.g., for static authentication tokens or routing). We will store and send these with each request, provided they don't conflict with essential headers.
Our service will send events to your configured Webhook URL using the following specifications:
Method: POST
Headers:
Content-Type: application/json
event-type: order)The request body is a complex JSON object representing the transformed and serialized order data. The following structure, but may be simplified for documentation clarity. Refer to specific fields relevant to your integration.
Key Fields & Notes:
Identifiers: referenceId, partnerId, retailers[].id, items[].id, etc., are our internal relevant identifiers.
Timestamps: All timestamps are in ISO 8601 format (UTC).
Payload Granularity: The payload contains details potentially aggregated across multiple internal entities (orders, shipments, fulfillments, retailers). The
Content-Type: application/json
User-Agent: liquid-commerce-event-bridge-service/1.0
x-liquid-commerce-event-type: e.g., orders
Your webhook endpoint must respond promptly to acknowledge receipt.
Success: Respond with an HTTP 2xx status code (e.g., 200 OK, 202 Accepted, 204 No Content).
Respond immediately after validating the signature (if applicable) and before performing complex/long-running business logic to avoid timeouts. Our service treats any 2xx as success for the delivery attempt.
If your endpoint:
Responds with a non-2xx status code.
Times out (default: 5000ms).
Experiences a network connectivity issue.
Our service will automatically retry 3 times with exponential backoff delays: 1000ms, 2000ms, 4000ms (approximately).
If the event delivery fails after the initial attempt and all 3 retries (total 4 attempts), the original event payload and error details will be sent to our internal Dead Letter Queue (DLQ) which purges after 7 days. No further delivery attempts will be made for that event. Consistent failures indicate an issue with your endpoint that needs investigation.
While we have internal mechanisms, the nature of our Distributed Event Streaming System with at-least-once delivery guarantee means you might receive the same logical update via multiple webhook calls.
It is CRITICAL that your system implements idempotency checks to prevent duplicate processing. Use a combination of identifiers from the payload relevant to your use case. A good candidate is the top-level referenceId combined with a relevant timestamp like updatedAt, or specific fulfillment/package IDs if processing at that level.
Example Check: Before processing, check if you've already successfully processed an update for referenceId with the same or an earlier updatedAt timestamp. If so, respond 2xx OK but skip processing.
You must verify the x-liquid-commerce-hmac-sha256 header on every incoming request using the unique Shared Secret we provide.
Algorithm: HMAC-SHA256
Encoding: Base64
Data Signed: The raw, unparsed JSON request body (exactly as received, including whitespace).
Verification Steps:
Retrieve the raw request body bytes.
Compute the HMAC-SHA256 hash of the raw body using the Shared Secret we provided.
Encode the resulting hash in Base64.
Securely compare (using a timing-safe comparison function) your calculated signature with the value from the x-liquid-commerce-hmac-sha256
Failure to implement and correctly verify this signature poses a significant security risk, potentially allowing attackers to send fraudulent data to your endpoint.
Here are conceptual examples demonstrating the core logic (signature verification, immediate acknowledgment, asynchronous processing) in various languages. These examples focus on the essential security and request handling aspects. Refer to the "Webhook Request Details" section for a comprehensive list of all headers your endpoint will receive, including informational headers like x-liquid-commerce-idempotency-key. Adapt the business logic, error handling, logging, security practices, and asynchronous mechanisms to your specific framework and production needs.
If you encounter issues during integration or have questions about the webhook format or behavior, please contact .
User-Agent: liquid-commerce-event-bridge-service/1.0x-liquid-commerce-event-type: The type of event (e.g., order).
x-liquid-commerce-hmac-sha256: The HMAC signature (see Security section).
x-liquid-commerce-idempotency-key: A unique identifier for this specific webhook delivery attempt, generated by our service.
x-liquid-commerce-delivery-id: A unique identifier for this delivery, constructed from order identifiers and a timestamp.
x-liquid-commerce-timestamp: An ISO 8601 timestamp indicating when the webhook was sent by our service.
Any custom headers you provided during setup.
retailersfulfillmentspackagesamountsStatus Fields: Note the different status enums used at different levels (e.g., fulfillments[].status vs. packages[].status). Use the status most relevant to your needs.
Amounts: Amounts are provided at the top level (order total) and broken down within amounts.taxDetails, amounts.discountDetails, and also within each retailers[].amounts. Item-level pricing is in items[].pricing.
Optional Fields: Many fields are optional (nullable) depending on the specific order details (e.g., giftMessage, deliveryInstructions, legacyOrderNumber, referenceId). At least referenceId or legacyOrderNumber will be included and for hybrid orders you'll receive both.
Additional Headers: Our service includes a few additional headers for traceability and idempotency assistance:
x-liquid-commerce-idempotency-key: A unique UUID for each delivery attempt. While you must implement your own idempotency based on payload content (like referenceId and updatedAt), this key can be useful for logging and tracing specific delivery attempts on your end.
x-liquid-commerce-delivery-id: A unique identifier for this delivery, typically formed using order identifiers and a timestamp. Useful for tracing.
x-liquid-commerce-timestamp: An ISO 8601 timestamp marking when the webhook request was initiated from our service.
x-liquid-commerce-hmac-sha256: (If secret provided) Base64 encoded HMAC-SHA256 signature.
x-liquid-commerce-idempotency-key: Unique identifier for the delivery attempt.
x-liquid-commerce-delivery-id: Unique identifier for the delivery.
x-liquid-commerce-timestamp: Timestamp of when the webhook was sent.
Any custom headers you specified.
Failure: Respond with an HTTP 4xx (Client Error) or 5xx (Server Error) status code if:
The signature verification fails (respond with 401 Unauthorized or 400 Bad Request).
Your service encounters an error preventing acceptance (e.g., temporary overload, validation error on your side).
{
referenceId: 'unique-order-ref-123', // Optional: Our internal unique order reference for new services orders
legacyOrderNumber: 'RBR-LEGACY-456', // Optional: Legacy order number if applicable
isHybrid: false, // Flag indicating if order involves legacy and new services components
partnerId: 'partner-id', // Your assigned Partner ID in our system
partnerName: 'Partner Xyz', // Your partner name in our system
createdAt: '2023-10-27T10:00:00.000Z', // ISO 8601 timestamp of order creation
updatedAt: '2023-10-27T10:30:00.000Z', // ISO 8601 timestamp of last relevant update triggering webhook
customer: {
id: 'cust-id',
firstName: 'Jane',
lastName: 'Doe',
email: '[email protected]',
phone: '(212) 290-9820',
birthdate: '1990-05-15', // YYYY-MM-DD
},
addresses: {
shipping: {
firstName: 'Jane',
lastName: 'Doe',
email: '[email protected]',
phone: '+15551234567',
company: null,
one: '123 Main St',
two: 'Apt 4B',
city: 'Anytown',
state: 'CA',
zip: '90210',
country: 'US',
},
billing: {
// ... similar structure, may differ from shipping ...
firstName: 'Jane',
lastName: 'Doe',
email: '[email protected]',
phone: '+15551234567',
company: null,
one: '123 Main St',
two: 'Apt 4B',
city: 'Anytown',
state: 'CA',
zip: '90210',
country: 'US',
},
},
options: {
isGift: true,
giftMessage: 'Happy Birthday!',
giftRecipient: {
name: 'John Smith',
email: '[email protected]',
phone: '+15559876543',
},
hasVerifiedAge: true,
allowsSubstitution: false,
billingSameAsShipping: true,
deliveryInstructions: 'Leave at front door.',
marketingPreferences: {
email: false,
sms: false,
},
},
amounts: {
subtotal: 10000,
shipping: 1000,
platform: 599, // Fee charged by our platform
tax: 850,
engraving: 500,
service: 200,
delivery: 0, // Specific delivery fee component
discounts: 1500, // Total discounts applied
giftCards: 2500, // Amount paid via gift cards
tip: 500,
total: 9150,
taxDetails: {
products: 700,
shipping: 100,
delivery: 0,
bag: 0,
bottleDeposits: 5,
retailDelivery: 0,
},
discountDetails: {
products: 1000,
shipping: 500,
delivery: 0,
engraving: 0,
service: 0,
},
},
paymentMethods: [
{
type: 'card', // e.g., card, paypal
card: 'Visa', // e.g., Visa, Mastercard
last4: '1234',
holder: 'Jane Doe',
code: null, // If a Gift Card was used an abstrct code will be available (ex: 'TER*****FH9W48Y')
},
],
retailers: [
{
id: 'retailer-id-1',
legacyId: 'legacy-retailer-abc',
name: 'Best Drinks Store',
system: 'LiquidCommerce OMS', // Actual values: LiquidCommerce OMS, ReserveBar OMS
timezone: 'America/Los_Angeles',
address: {
one: '456 Side St',
two: null,
city: 'Anytown',
state: 'CA',
zip: '90211',
country: 'US',
coordinates: {
latitude: 34.0522,
longitude: -118.2437,
},
},
amounts: {
// Amounts specific to this retailer's fulfillment
// ... structure similar to top-level amounts ...
subtotal: 6000,
shipping: 500,
platform: 599,
tax: 510,
engraving: 5000,
service: 120,
delivery: 0,
discounts: 1000,
giftCards: 1000,
tip: 250,
total: 5940,
taxDetails: {
products: 700,
shipping: 100,
delivery: 0,
bag: 0,
bottleDeposits: 5,
retailDelivery: 0,
},
},
discountDetails: {
products: 1000,
shipping: 500,
delivery: 0,
engraving: 0,
service: 0,
},
},
fulfillments: [
{
id: 'fulfillment-id-xyz',
type: 'onDemand', // Actual values: shipping, onDemand, digital, bopis
status: 'processing', // Actual values: created, processing, inTransit, canceled, delivered
scheduledFor: null, // ISO 8601 if scheduled
updatedAt: '2023-10-27T10:25:00.000Z',
cancellation: {
category: null,
subcategory: null,
notes: null,
},
expectationFormatted: {
detail: "Ships in 1-2 days",
engraving: "Ships in 10 days",
},
itemIds: ['item-id-1', 'item-id-2'],
packages: [
{
id: 'package-id-123',
carrier: 'UPS',
trackingNumber: '1Z999AA10123456784',
trackingUrl: 'https://wwwapps.ups.com/...',
status: 'shipped', // Actual values: created, processing, inTransit, canceled, delivered
dateShipped: '2023-10-27T10:25:00.000Z',
},
],
timeline: [
{ status: 'created', timestamp: '2023-10-27T10:00:00.000Z' }, // Actual values for status: created, scheduled, onHold, pending, processing, inTransit, delivered, canceled
{ status: 'processing', timestamp: '2023-10-27T10:15:00.000Z' },
// Potentially more status updates (inTransit, delivered, etc.)
],
},
// Potentially multiple fulfillments per retailer if split
],
},
// Potentially multiple retailers if multi-retailer order
],
items: [
{
id: 'item-id-1',
fulfillmentId: 'fulfillment-id-xyz',
retailerId: 'retailer-id-1',
variantId: 'variant-id-1',
liquidId: 'liquid-id-123',
legacyGrouping: null,
legacyPid: 'pid-abc',
expectation: "Ships in 1-2 days", // expectation formatted for a specific item
customerPlacement: 'standard', // Actual values for status: standard, pre_sale, back_order
isPresale: false,
estimatedShipBy: null,
product: {
name: 'Example Wine 750ml',
brand: 'Example Vineyards',
upc: '012345678912',
sku: 'EXWINE750',
mskus: ['RETAILER123'],
category: 'Red Wine',
size: '750ml',
volume: '750',
uom: 'ml',
proof: null,
attributes: {
pack: false,
packDescription: null,
abv: '13.5',
container: 'Bottle',
containerType: 'Glass',
},
},
image: 'https://example.com/image.jpg',
pricing: {
price: 2000,
unitPrice: 2000,
quantity: 2,
tax: 340,
bottleDeposits: 20,
},
attributes: {
engraving: {
hasEngraving: true,
fee: 5000,
location: 'Front',
lines: ['Line 1 Text', 'Line 2 Text'],
},
giftCard: {
// Based on PartnerOrderItemGiftCardSerialization
sender: 'Jane Doe',
message: null,
recipients: ['John Smith <[email protected]>'],
sendDate: null,
},
},
},
// ... more items ...
],
}import express from 'express';
import crypto from 'crypto';
const app = express();
// IMPORTANT: Use express.raw middleware to access the raw request body for HMAC verification.
// Must run *before* express.json() or any other body-parsing middleware.
app.use(express.raw({ type: 'application/json', limit: '5mb' })); // Adjust limit as needed
const PORT = process.env.PORT || 3000;
// Store the unique secret WE PROVIDED securely (e.g., in environment variables)
const WEBHOOK_SECRET = process.env.EVENT_BRIDGE_WEBHOOK_SECRET;
const HMAC_HEADER = 'x-liquid-commerce-hmac-sha256'; // Use the correct header name
// Your Webhook Endpoint
app.post('/webhook/event-bridge', (req, res) => {
console.log(`Received webhook request headers: ${JSON.stringify(req.headers)}`);
// --- 1. Verify Signature (CRITICAL) ---
if (!WEBHOOK_SECRET) {
console.error(`${HMAC_HEADER} verification skipped: WEBHOOK_SECRET not configured!`);
// Consider failing requests if secret is expected but not configured
return res.status(500).send('Internal Server Error: Webhook secret not configured.');
}
const receivedSignature = req.header(HMAC_HEADER);
if (!receivedSignature) {
console.warn(`Missing ${HMAC_HEADER} header`);
return res.status(400).send('Bad Request: Missing signature header');
}
try {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body) // Use the raw body buffer from express.raw()
.digest('base64');
const receivedSigBuffer = Buffer.from(receivedSignature, 'base64');
const expectedSigBuffer = Buffer.from(expectedSignature, 'base64');
// Use timing-safe comparison
if (
receivedSigBuffer.length !== expectedSigBuffer.length ||
!crypto.timingSafeEqual(receivedSigBuffer, expectedSigBuffer)
) {
console.error('Invalid signature');
return res.status(401).send('Unauthorized: Invalid signature');
}
console.log('Signature verified successfully');
} catch (error) {
console.error('Error during signature verification:', error);
return res.status(500).send('Internal Server Error during signature verification');
}
// --- 2. Acknowledge Receipt Immediately (Before Parsing/Processing) ---
// Send a 2xx response quickly.
res.status(202).send('Accepted');
// --- 3. Parse and Process Asynchronously ---
// Wrap parsing and processing in a separate async function to avoid blocking.
handleEventProcessing(req.body)
.then(() => console.log('Event processing initiated.'))
.catch((err) => console.error('Failed to initiate event processing:', err));
});
// Asynchronous function to handle parsing and business logic
async function handleEventProcessing(rawBodyBuffer) {
let eventPayload;
try {
// Parse the JSON from the raw body buffer
eventPayload = JSON.parse(rawBodyBuffer.toString('utf-8'));
// console.log('Parsed payload:', JSON.stringify(eventPayload, null, 2)); // Be careful logging full payload
console.log(`Parsed payload for referenceId: ${eventPayload?.referenceId}`);
} catch (error) {
console.error('Failed to parse JSON payload after acknowledgment:', error);
// Add monitoring/alerting here. The request was already acknowledged.
return; // Stop processing if parsing fails
}
console.log(`Processing event referenceId: ${eventPayload.referenceId}, UpdatedAt: ${eventPayload.updatedAt}`);
// --- Implement Idempotency Check Here ---
// const alreadyProcessed = await checkIfEventProcessed(eventPayload.referenceId, eventPayload.updatedAt);
// if (alreadyProcessed) {
// console.log(`Event referenceId ${eventPayload.referenceId} already processed or is older. Skipping.`);
// return;
// }
// --- Your Business Logic Here ---
// e.g., update your database based on eventPayload details...
// Use eventPayload.retailers[0].fulfillments[0].status or packages[0].status etc.
try {
// ... Simulate async work ...
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(`Business logic completed for referenceId: ${eventPayload.referenceId}`);
} catch (businessError) {
console.error(`Error during business logic for referenceId ${eventPayload.referenceId}:`, businessError);
// Add monitoring/alerting. Event already acknowledged. May need manual reconciliation.
return;
}
// --- Mark event as processed (for idempotency) ---
// await markEventAsProcessed(eventPayload.referenceId, eventPayload.updatedAt);
console.log(`Successfully processed event referenceId: ${eventPayload.referenceId}`);
}
app.listen(PORT, () => {
console.log(`Webhook receiver listening on port ${PORT}`);
if (!WEBHOOK_SECRET) {
console.warn(
`Warning: EVENT_BRIDGE_WEBHOOK_SECRET environment variable not set. ${HMAC_HEADER} verification will be skipped unless the check inside verify_signature fails.`,
);
}
});import os
import hmac
import hashlib
import base64
import json
import logging
from flask import Flask, request, abort, make_response
from threading import Thread
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# Store the unique secret WE PROVIDED securely (e.g., in environment variables)
WEBHOOK_SECRET = os.environ.get('EVENT_BRIDGE_WEBHOOK_SECRET')
HMAC_HEADER = 'x-liquid-commerce-hmac-sha256' # Use the correct header name
def verify_signature(req):
"""Verifies the HMAC signature of the request."""
if not WEBHOOK_SECRET:
logging.error(f"{HMAC_HEADER} verification skipped: WEBHOOK_SECRET not configured!")
# Fail closed if secret is expected but missing
abort(500, description='Internal Server Error: Webhook secret not configured.')
received_signature_b64 = req.headers.get(HMAC_HEADER)
if not received_signature_b64:
logging.warning(f"Missing {HMAC_HEADER} header")
abort(400, description=f'Bad Request: Missing {HMAC_HEADER} header')
# IMPORTANT: Access the raw request body bytes
raw_body = req.get_data() # Flask specific way to get raw body
try:
hash_obj = hmac.new(WEBHOOK_SECRET.encode('utf-8'), raw_body, hashlib.sha256)
expected_signature_b64 = base64.b64encode(hash_obj.digest()).decode('utf-8')
# Use timing-safe comparison
if not hmac.compare_digest(expected_signature_b64, received_signature_b64):
logging.error('Invalid signature')
abort(401, description='Unauthorized: Invalid signature')
logging.info('Signature verified successfully')
return raw_body # Return raw body for later processing
except Exception as e:
logging.error(f"Error during signature verification: {e}", exc_info=True)
abort(500, description='Internal Server Error during signature verification')
def process_event_async(raw_body_bytes):
"""Parses and processes the event payload asynchronously."""
try:
payload_str = raw_body_bytes.decode('utf-8')
event_payload = json.loads(payload_str)
reference_id = event_payload.get('referenceId', 'N/A')
updated_at = event_payload.get('updatedAt', 'N/A')
logging.info(f"Parsed payload for referenceId: {reference_id}")
# Careful logging full payload in production
# logging.debug(f"Full payload: {payload_str}")
except json.JSONDecodeError as e:
logging.error(f"Failed to parse JSON payload after acknowledgment: {e}", exc_info=True)
# Add monitoring/alerting here
return # Stop processing if parsing fails
except Exception as e:
logging.error(f"Unexpected error during payload parsing: {e}", exc_info=True)
return
logging.info(f"Processing event referenceId: {reference_id}, UpdatedAt: {updated_at}")
# --- Implement Idempotency Check Here ---
# try:
# already_processed = check_if_event_processed(reference_id, updated_at)
# if already_processed:
# logging.info(f"Event referenceId {reference_id} already processed or is older. Skipping.")
# return
# except Exception as e:
# logging.error(f"Error during idempotency check for referenceId {reference_id}: {e}", exc_info=True)
# # Decide how to handle idempotency check errors (e.g., retry, alert)
# return
# --- Your Business Logic Here ---
try:
# e.g., update your database based on event_payload details...
# Simulate work
import time
time.sleep(0.1) # Simulate async work
logging.info(f"Business logic completed for referenceId: {reference_id}")
except Exception as business_error:
logging.error(f"Error during business logic for referenceId {reference_id}: {business_error}", exc_info=True)
# Add monitoring/alerting. Event already acknowledged. May need manual reconciliation.
return
# --- Mark event as processed (for idempotency) ---
# try:
# mark_event_as_processed(reference_id, updated_at)
# logging.info(f"Successfully marked referenceId {reference_id} as processed.")
# except Exception as e:
# logging.error(f"Error marking referenceId {reference_id} as processed: {e}", exc_info=True)
# # Alerting is crucial here if marking fails
@app.route('/webhook/event-bridge', methods=['POST'])
def handle_webhook():
logging.info(f"Received webhook request headers: {dict(request.headers)}")
# --- 1. Verify Signature (CRITICAL) ---
# This function calls abort() on failure
raw_body = verify_signature(request)
# --- 2. Acknowledge Receipt Immediately (Before Parsing/Processing) ---
# Send a 2xx response quickly.
response = make_response("Accepted", 202)
# --- 3. Start Asynchronous Processing ---
# Use threading for simple async background task (for production, consider Celery, RQ, etc.)
thread = Thread(target=process_event_async, args=(raw_body,))
thread.start()
logging.info("Event processing initiated in background thread.")
return response
if __name__ == '__main__':
port = int(os.environ.get('PORT', 3000))
if not WEBHOOK_SECRET:
logging.warning(f"Warning: EVENT_BRIDGE_WEBHOOK_SECRET environment variable not set. {HMAC_HEADER} verification will be skipped unless the check inside verify_signature fails.")
# Use a production-ready WSGI server like gunicorn or uWSGI instead of app.run in production
app.run(host='0.0.0.0', port=port)
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
const hmacHeader = "x-liquid-commerce-hmac-sha256" // Use the correct header name
var webhookSecret string // Store the unique secret WE PROVIDED
func init() {
webhookSecret = os.Getenv("EVENT_BRIDGE_WEBHOOK_SECRET")
if webhookSecret == "" {
log.Printf("Warning: EVENT_BRIDGE_WEBHOOK_SECRET environment variable not set. %s verification will fail.", hmacHeader)
// Consider os.Exit(1) if secret is mandatory for startup
}
}
func verifySignature(r *http.Request, rawBody []byte) (bool, error) {
if webhookSecret == "" {
log.Printf("Error: %s verification skipped: WEBHOOK_SECRET not configured!", hmacHeader)
return false, fmt.Errorf("webhook secret not configured") // Fail closed
}
receivedSignatureB64 := r.Header.Get(hmacHeader)
if receivedSignatureB64 == "" {
log.Printf("Warning: Missing %s header", hmacHeader)
return false, fmt.Errorf("missing signature header")
}
mac := hmac.New(sha256.New, []byte(webhookSecret))
_, err := mac.Write(rawBody) // Use the raw body bytes
if err != nil {
log.Printf("Error writing body to HMAC: %v", err)
return false, fmt.Errorf("HMAC calculation error")
}
expectedSignatureBytes := mac.Sum(nil)
expectedSignatureB64 := base64.StdEncoding.EncodeToString(expectedSignatureBytes)
// Decode received signature for timing-safe comparison
receivedSignatureBytes, err := base64.StdEncoding.DecodeString(receivedSignatureB64)
if err != nil {
log.Printf("Error decoding received signature: %v", err)
return false, fmt.Errorf("invalid signature format")
}
// Use timing-safe comparison
if subtle.ConstantTimeCompare(receivedSignatureBytes, expectedSignatureBytes) == 1 {
log.Println("Signature verified successfully")
return true, nil
}
log.Println("Invalid signature")
// Log expected vs received for debugging (CAUTION: Sensitive in prod logs)
// log.Printf("Expected: %s, Received (b64): %s", expectedSignatureB64, receivedSignatureB64)
return false, fmt.Errorf("invalid signature")
}
func handleEventProcessingAsync(rawBodyBytes []byte) {
var eventPayload map[string]interface{} // Use a more specific struct in production
err := json.Unmarshal(rawBodyBytes, &eventPayload)
if err != nil {
log.Printf("Failed to parse JSON payload after acknowledgment: %v", err)
// Add monitoring/alerting here
return // Stop processing if parsing fails
}
referenceID := "N/A"
updatedAt := "N/A"
if refID, ok := eventPayload["referenceId"].(string); ok {
referenceID = refID
}
if updAt, ok := eventPayload["updatedAt"].(string); ok {
updatedAt = updAt
}
log.Printf("Parsed payload for referenceId: %s", referenceID)
// Careful logging full payload in production
// log.Printf("Full payload: %s", string(rawBodyBytes))
log.Printf("Processing event referenceId: %s, UpdatedAt: %s", referenceID, updatedAt)
// --- Implement Idempotency Check Here ---
// alreadyProcessed, err := checkIfEventProcessed(referenceID, updatedAt)
// if err != nil {
// log.Printf("Error during idempotency check for referenceId %s: %v", referenceID, err)
// // Decide how to handle idempotency check errors
// return
// }
// if alreadyProcessed {
// log.Printf("Event referenceId %s already processed or is older. Skipping.", referenceID)
// return
// }
// --- Your Business Logic Here ---
// e.g., update your database based on eventPayload details...
// Simulate work
time.Sleep(100 * time.Millisecond) // Simulate async work
log.Printf("Business logic completed for referenceId: %s", referenceID)
// --- Mark event as processed (for idempotency) ---
// err = markEventAsProcessed(referenceID, updatedAt)
// if err != nil {
// log.Printf("Error marking referenceId %s as processed: %v", referenceID, err)
// // Alerting is crucial here
// } else {
// log.Printf("Successfully marked referenceId %s as processed.", referenceID)
// }
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Received webhook request headers: %v", r.Header)
if r.Method != http.MethodPost {
log.Printf("Invalid method: %s", r.Method)
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// IMPORTANT: Read the raw request body
// Limit body size to prevent abuse (e.g., 5MB)
r.Body = http.MaxBytesReader(w, r.Body, 5*1024*1024)
rawBody, err := io.ReadAll(r.Body)
if err != nil {
if err.Error() == "http: request body too large" {
log.Printf("Request body too large: %v", err)
http.Error(w, "Request Entity Too Large", http.StatusRequestEntityTooLarge)
} else {
log.Printf("Error reading request body: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}
// It's crucial to close the original body, although ReadAll does effectively consume it.
// If you were using io.Copy or similar, ensure the original body is closed.
defer r.Body.Close()
// --- 1. Verify Signature (CRITICAL) ---
valid, sigErr := verifySignature(r, rawBody)
if !valid {
// Determine appropriate status code based on error
statusCode := http.StatusInternalServerError
if sigErr != nil {
errMsg := sigErr.Error()
if errMsg == "missing signature header" {
statusCode = http.StatusBadRequest
} else if errMsg == "invalid signature format" || errMsg == "invalid signature" {
statusCode = http.StatusUnauthorized
} else if errMsg == "webhook secret not configured" {
statusCode = http.StatusInternalServerError // Or 503 Service Unavailable
}
}
http.Error(w, fmt.Sprintf("%d %s", statusCode, http.StatusText(statusCode)), statusCode)
return
}
// --- 2. Acknowledge Receipt Immediately (Before Parsing/Processing) ---
// Send a 2xx response quickly.
w.WriteHeader(http.StatusAccepted)
fmt.Fprintln(w, "Accepted")
// --- 3. Start Asynchronous Processing ---
// Use a goroutine for async background task
go handleEventProcessingAsync(rawBody)
log.Println("Event processing initiated in background goroutine.")
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
http.HandleFunc("/webhook/event-bridge", webhookHandler)
log.Printf("Webhook receiver listening on port %s", port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import jakarta.servlet.http.HttpServletRequest; // Use jakarta for Spring Boot 3+
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Objects;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map; // Or a specific DTO class
@SpringBootApplication
@EnableAsync // Enable asynchronous processing
public class WebhookReceiverApplication {
private static final Logger log = LoggerFactory.getLogger(WebhookReceiverApplication.class);
// Store the unique secret WE PROVIDED securely (e.g., via @Value or config server)
private static final String WEBHOOK_SECRET = System.getenv("EVENT_BRIDGE_WEBHOOK_SECRET");
private static final String HMAC_HEADER = "x-liquid-commerce-hmac-sha256"; // Use the correct header name
private static final String HMAC_ALGORITHM = "HmacSHA256";
public static void main(String[] args) {
if (WEBHOOK_SECRET == null || WEBHOOK_SECRET.isEmpty()) {
log.warn("Warning: EVENT_BRIDGE_WEBHOOK_SECRET environment variable not set. {} verification will fail.", HMAC_HEADER);
// Potentially prevent application startup if secret is mandatory
}
SpringApplication.run(WebhookReceiverApplication.class, args);
}
@RestController
public static class WebhookController {
private final SignatureVerifier signatureVerifier;
private final AsyncEventProcessor asyncEventProcessor;
public WebhookController(SignatureVerifier signatureVerifier, AsyncEventProcessor asyncEventProcessor) {
this.signatureVerifier = signatureVerifier;
this.asyncEventProcessor = asyncEventProcessor;
}
// IMPORTANT: Receive raw body as byte array
@PostMapping("/webhook/event-bridge")
public ResponseEntity<String> handleWebhook(@RequestBody byte[] rawBody, HttpServletRequest request) {
log.info("Received webhook request headers: {}", request.getHeaderNames()); // Log header names or specific needed headers
String receivedSignature = request.getHeader(HMAC_HEADER);
// --- 1. Verify Signature (CRITICAL) ---
if (!signatureVerifier.isValidSignature(rawBody, receivedSignature)) {
// SignatureVerifier logs details; determine status code based on internal state/error
if (receivedSignature == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Bad Request: Missing signature header");
} else if (WEBHOOK_SECRET == null || WEBHOOK_SECRET.isEmpty()){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal Server Error: Webhook secret not configured.");
}
else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: Invalid signature");
}
}
// --- 2. Acknowledge Receipt Immediately (Before Parsing/Processing) ---
ResponseEntity<String> response = ResponseEntity.status(HttpStatus.ACCEPTED).body("Accepted");
// --- 3. Start Asynchronous Processing ---
asyncEventProcessor.handleEventProcessing(rawBody);
log.info("Event processing initiated asynchronously.");
return response;
}
}
@Component
public static class SignatureVerifier {
public boolean isValidSignature(byte[] rawBody, String receivedSignatureB64) {
if (WEBHOOK_SECRET == null || WEBHOOK_SECRET.isEmpty()) {
log.error("{} verification skipped: WEBHOOK_SECRET not configured!", HMAC_HEADER);
// Fail closed if secret is expected but missing
return false;
}
if (receivedSignatureB64 == null || receivedSignatureB64.isEmpty()) {
log.warn("Missing {} header", HMAC_HEADER);
return false;
}
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(secretKeySpec);
byte[] expectedSignatureBytes = mac.doFinal(rawBody);
// No need to Base64 encode expected here, decode received instead for comparison
byte[] receivedSignatureBytes = Base64.getDecoder().decode(receivedSignatureB64);
// Use timing-safe comparison
boolean valid = MessageDigest.isEqual(receivedSignatureBytes, expectedSignatureBytes);
if (valid) {
log.info("Signature verified successfully");
} else {
log.error("Invalid signature");
// String expectedSignatureB64 = Base64.getEncoder().encodeToString(expectedSignatureBytes);
// log.debug("Expected B64: {}, Received B64: {}", expectedSignatureB64, receivedSignatureB64); // CAUTION in prod
}
return valid;
} catch (NoSuchAlgorithmException | InvalidKeyException | IllegalArgumentException e) {
log.error("Error during signature verification: {}", e.getMessage(), e);
return false; // Treat crypto errors as invalid signature
}
}
}
@Service
public static class AsyncEventProcessor {
private final ObjectMapper objectMapper = new ObjectMapper(); // Jackson mapper
@Async // Make this method run in a background thread pool
public void handleEventProcessing(byte[] rawBodyBytes) {
Map<String, Object> eventPayload; // Use a specific DTO class for better type safety
String referenceId = "N/A";
String updatedAt = "N/A";
try {
eventPayload = objectMapper.readValue(rawBodyBytes, Map.class);
// Extract relevant fields safely
if(eventPayload.get("referenceId") instanceof String) {
referenceId = (String) eventPayload.get("referenceId");
}
if(eventPayload.get("updatedAt") instanceof String) {
updatedAt = (String) eventPayload.get("updatedAt");
}
log.info("Parsed payload for referenceId: {}", referenceId);
// Be careful logging full payload in production
// log.debug("Full payload: {}", new String(rawBodyBytes, StandardCharsets.UTF_8));
} catch (JsonProcessingException e) {
log.error("Failed to parse JSON payload after acknowledgment: {}", e.getMessage(), e);
// Add monitoring/alerting here
return; // Stop processing if parsing fails
} catch (Exception e) {
log.error("Unexpected error during payload parsing: {}", e.getMessage(), e);
return;
}
log.info("Processing event referenceId: {}, UpdatedAt: {}", referenceId, updatedAt);
// --- Implement Idempotency Check Here ---
// try {
// boolean alreadyProcessed = checkIfEventProcessed(referenceId, updatedAt); // Inject service dependency
// if (alreadyProcessed) {
// log.info("Event referenceId {} already processed or is older. Skipping.", referenceId);
// return;
// }
// } catch (Exception e) {
// log.error("Error during idempotency check for referenceId {}: {}", referenceId, e.getMessage(), e);
// // Decide how to handle idempotency check errors
// return;
// }
// --- Your Business Logic Here ---
try {
// e.g., update your database based on eventPayload details...
// Simulate work
Thread.sleep(100); // Simulate async work
log.info("Business logic completed for referenceId: {}", referenceId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Business logic interrupted for referenceId {}: {}", referenceId, e.getMessage());
return;
} catch (Exception businessError) {
log.error("Error during business logic for referenceId {}: {}", referenceId, businessError.getMessage(), businessError);
// Add monitoring/alerting. Event already acknowledged. May need manual reconciliation.
return;
}
// --- Mark event as processed (for idempotency) ---
// try {
// markEventAsProcessed(referenceId, updatedAt); // Inject service dependency
// log.info("Successfully marked referenceId {} as processed.", referenceId);
// } catch (Exception e) {
// log.error("Error marking referenceId {} as processed: {}", referenceId, e.getMessage(), e);
// // Alerting is crucial here
// }
}
}
}<?php declare(strict_types=1);
// Basic error logging (use a proper logger like Monolog in production)
error_reporting(E_ALL);
ini_set('log_errors', '1');
ini_set('error_log', '/tmp/php_webhook_errors.log'); // Adjust path
// --- Configuration ---
$webhookSecret = getenv('EVENT_BRIDGE_WEBHOOK_SECRET') ?: ''; // Store the unique secret WE PROVIDED securely
$hmacHeader = 'x-liquid-commerce-hmac-sha256'; // Use the correct header name
// --- Request Handling ---
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$headers = getallheaders(); // Get all request headers
// Log headers (be careful in production)
// file_put_contents('php://stderr', 'Received headers: ' . json_encode($headers) . PHP_EOL);
if ($requestMethod !== 'POST') {
error_log('Invalid method: ' . $requestMethod);
http_response_code(405); // Method Not Allowed
echo 'Method Not Allowed';
exit;
}
// --- 1. Verify Signature (CRITICAL) ---
if (empty($webhookSecret)) {
error_log("{$hmacHeader} verification skipped: WEBHOOK_SECRET not configured!");
http_response_code(500);
echo 'Internal Server Error: Webhook secret not configured.';
exit;
}
$receivedSignatureB64 = $headers[$hmacHeader] ?? '';
if (empty($receivedSignatureB64)) {
error_log("Missing {$hmacHeader} header");
http_response_code(400);
echo "Bad Request: Missing {$hmacHeader} header";
exit;
}
// IMPORTANT: Read the raw request body
$rawBody = file_get_contents('php://input');
if ($rawBody === false) {
error_log("Failed to read raw request body");
http_response_code(500);
echo 'Internal Server Error: Cannot read request body';
exit;
}
try {
$expectedSignature = hash_hmac('sha256', $rawBody, $webhookSecret, true); // true for raw binary output
$expectedSignatureB64 = base64_encode($expectedSignature);
// Use timing-safe comparison (hash_equals)
if (!hash_equals($expectedSignatureB64, $receivedSignatureB64)) {
error_log('Invalid signature');
// Debugging (CAUTION in production):
// error_log("Expected B64: " . $expectedSignatureB64);
// error_log("Received B64: " . $receivedSignatureB64);
http_response_code(401);
echo 'Unauthorized: Invalid signature';
exit;
}
error_log('Signature verified successfully'); // Use info level in proper logger
} catch (\Exception $e) {
error_log('Error during signature verification: ' . $e->getMessage());
http_response_code(500);
echo 'Internal Server Error during signature verification';
exit;
}
// --- 2. Acknowledge Receipt Immediately (Before Parsing/Processing) ---
// Send a 2xx Accepted response quickly.
http_response_code(202);
echo 'Accepted';
// Flush output buffers to ensure the response is sent before potentially long processing
if (function_exists('fastcgi_finish_request')) {
// Good practice for PHP-FPM to release connection
fastcgi_finish_request();
} elseif ('cli' !== PHP_SAPI) {
// Fallback for other SAPIs (might not work perfectly)
ob_start(); // Start output buffering if not already started
header('Connection: close');
header('Content-Length: ' . ob_get_length());
ob_end_flush();
flush();
}
// --- 3. Parse and Process (Ideally Asynchronously) ---
// WARNING: PHP typically processes requests synchronously. Long-running tasks here
// can block new requests depending on the server setup.
// For production, offload this processing to a background job queue (e.g., Redis Queue, Beanstalkd, RabbitMQ).
try {
$eventPayload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
$referenceId = $eventPayload['referenceId'] ?? 'N/A';
$updatedAt = $eventPayload['updatedAt'] ?? 'N/A';
error_log("Parsed payload for referenceId: {$referenceId}");
// Careful logging full payload in production
// error_log("Full payload: {$rawBody}");
error_log("Processing event referenceId: {$referenceId}, UpdatedAt: {$updatedAt}");
// --- Implement Idempotency Check Here ---
// $alreadyProcessed = checkIfEventProcessed($referenceId, $updatedAt); // Implement this function
// if ($alreadyProcessed) {
// error_log("Event referenceId {$referenceId} already processed or is older. Skipping.");
// exit; // Exit script cleanly
// }
// --- Your Business Logic Here ---
// e.g., update your database based on eventPayload details...
// Simulate work
sleep(1); // Simulate work (DON'T DO THIS IN PRODUCTION REQUEST HANDLER)
error_log("Business logic completed for referenceId: {$referenceId}");
// --- Mark event as processed (for idempotency) ---
// markEventAsProcessed($referenceId, $updatedAt); // Implement this function
// error_log("Successfully marked referenceId {$referenceId} as processed.");
} catch (\JsonException $e) {
error_log('Failed to parse JSON payload after acknowledgment: ' . $e->getMessage());
// Add monitoring/alerting here
exit; // Stop processing if parsing fails
} catch (\Throwable $e) { // Catch any other errors during processing
error_log("Error during business logic/idempotency for referenceId {$referenceId}: {$e->getMessage()} " . $e->getTraceAsString());
// Add monitoring/alerting. Event already acknowledged. May need manual reconciliation.
exit;
}
error_log("Successfully processed event referenceId: {$referenceId} (end of script)"); // Should match logged success above
// Helper function if not using Apache/Nginx specific ways
if (!function_exists('getallheaders')) {
function getallheaders(): array {
$headers = [];
foreach ($_SERVER as $name => $value) {
if (str_starts_with($name, 'HTTP_')) {
$headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
$headers[$headerName] = $value;
} elseif (in_array($name, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
$headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
$headers[$headerName] = $value;
}
}
return $headers;
}
}
?>