About PushCart
Push-Cart is a collection of services and APIs provided by Locally. These services allow retailers to receive real-time transactions from the Locally platform as they occur, effectively “pushing” the shopper’s cart to an external system such as a Point of Sale, Sales Order Management, or ERP for fulfillment.
Prerequisites
Inventory Integration or Inventory Feeds
Locally offers several ways to sync store inventory. If your system is already sending on-hand inventory to Locally, no action is required. Here is an example inventory feed format. The format is simple:
upc | qty | price |
---|---|---|
727602259178 | 1 | 10.99 |
605284574539 | 2 | 99.95 |
736745040314 | 100 | 147.50 |
In order to use PushCart your system must be updating inventory feeds at least once every 24 hours. If Locally detects feed staleness beyond 72 hours, PushCart will be temporarily disabled. Inventory setup is outside of the scope of this document. Please review our documentation here or contact your Locally support representative about getting started.
Participation in “Buy It Locally” (BIL)
Locally also requires the activation of “Buy It Locally” for all retailers in order to participate in the PushCart transaction workflow. PushCart supports Reserve Online Pickup in Store (ROPIS) and Buy Online Pickup In Store (BOPIS) transactions. To be eligible, stores may accept ROPIS, BOPIS, or both.
PushCart does not currently support Same-Day Delivery or Ship to Store orders. Retailers utilizing Pushcart that wish to offer those transaction types can still fulfill those orders from the Locally order management console in their Locally dashboard.
Before proceeding with your PushCart integration, ensure you’ve completed the setup steps.
Configuring Your PushCart Profile
If you don’t already have a Locally company account, please contact your Locally representative about getting an account set up.
Once your account is set up, the following basic settings can be adjusted by contacting Locally.
Webhook URL for Order Updates
You will need to provide Locally with a webhook URL endpoint. This URL can be changed at any time via the configuration panel. Locally will send a payload containing the cart
object (see below) to this endpoint whenever a new order is created or has been updated. It will be your company’s responsibility to consume this payload and act accordingly in order to fulfill the request in a timely manner. See the “Buy it Locally: Real-time Carts API” section below for more information on this topic.
Optionally, you may instead choose to poll our API at a high frequency. Please contact Locally to discuss if this option is feasible for your needs.
Manage Participating Stores
You can define which stores will have the PushCart capability. Any stores that are not enabled for PushCart fulfillment will default to the standard Buy it Locally customer order workflow (if any).
Buy It Locally: Real-time Carts API
The Real-time Carts API is your way to request up-to-the-minute information on all orders you have access to, and how to update an existing order’s status in the Locally system.
Requesting your API key
You will need an API Key to access your CartAPI endpoint. See Generate An API Key for more info.
GET /carts/{page}
endpoint
/carts/{page}
endpointNow that you have your API key, you will be able to access our Cart API endpoints.
A request is built by adding “Locally-API-Token” to the header of your request and the value of that will be the API key you generated on our platform.
To access the list of all the carts, use:
GET https://www.locally.com/api/v2/carts
You will need to pass along your API key in the header of the request. For example, in JSON:
{
"url": "<https://www.locally.com/api/v2/carts">,
"raw_url": "<https://www.locally.com/api/v2/carts">,
"method": "get",
"headers": {
"Locally-API-Token": "ea432b89353efxxxe46f147b3uae37fgcae2337"
}
}
This will return the list of all carts your account has access to, ordered by most recent first, and broken up via 50 per page. The first page can be accessed via just /api/v2/carts
or /api/v2/carts/0
.
Sample payload response:
{
"status": true,
"properties": {
"date_time": "2022-02-03 10:52:40",
"current_page": 0,
"total_pages": 3
},
"carts": [
{
"hash": "E3GKY8",
"type": "HOLD",
"n_items": 1,
"source": "From locally.com",
"ordered_at": "2022-01-28 10:28:22",
"updated_at": "2022-01-28 15:34:30",
"delivery_status": "",
"pickup_window": "2022-01-29 15:00:00",
"items": [
{
"currency": "EUR",
"total": "100.00",
"status": "Canceled",
"sts_status": "N/A",
"qty": 1,
"product_id": 967586,
"order_resolution": "auto-cancelled",
"upc": "601842495575",
"alias":[
"D1447020122-00003"
]
}
]
},
{
"hash": "OK38N3",
"type": "ORDER",
"n_items": 1,
"source": "From locally.com",
"ordered_at": "2021-11-18 15:08:35",
"updated_at": "2021-11-18 15:09:59",
"delivery_status": "",
"pickup_window": "2022-11-19 15:00:00",
"items": [
{
"currency": "JPY",
"total": "990.00",
"status": "Confirmed",
"sts_status": "N/A",
"qty": 1,
"product_id": 936676,
"order_resolution": "",
"upc": "745889071559",
"alias": []
}
]
}
]
}
Definition of the response payload:
Field | Description |
---|---|
status | Boolean. true if request was a success, false if not. |
message | Only sent if status was false, gives the reason for the false status. |
properties.date_time | The datetime of this request. |
properties.current_page | The current page you are on. Pagination starts at page 0 (zero). |
properties.total_pages | Total number of pages. If total pages is 5 , then pages 0, 1, 2, 3, 4 exist. |
carts | Array of carts. |
carts[].hash | The unique hash for the order. For example E3GKY8 . |
carts[].type | HOLD for a reservation for in-store payment.ORDER for a payment for in-store pickup.SHIP for a Ship-to-Store order (This is a special case for select brands only. This transaction type is NOT available for POS vendor or retailer integrations). |
carts[].n_items | Number of items in the order. |
carts[].source | Domain where the order originated from. “From Locally.com”, “From brand-site.com”, etc. |
carts[].ordered_at | The datetime of when the order was placed. |
carts[].updated_at | The datetime of when the order has changed last. |
carts[].delivery_status | If the cart is a Same-Day Delivery, this is its current status. PushCart does not support Same-Day Delivery at this time, so this will be blank. |
carts[].pickup_window | The datetime string of the selected pickup window slot time or empty string. |
carts[].items | Breakdown of items in carts statuses and basic information. Please note that there can be more than one item per cart. |
carts[].items[].currency | The currency the order is in. Adheres to ISO 4217 |
carts[].items[].total | The total amount for the item. Includes tax if this is a "Buy Online, Pickup in Store (BOPIS)" order. |
carts[].items[].status | The status of the item:Ordered : The BOPIS or ROPIS request has been placed and has not been resolved by the store.Confirmed : The store has confirmed that the item is in stock and is being held for the shopper.Canceled : The store has rejected the BOPIS or ROPIS request.Charge_Failed : There was an issue charging the shopper’s credit card on a BOPIS order and payment was not captured. This is rare but should be accounted for. The store should treat these like reservation requests and collect payment when the shopper arrives. |
carts[].items[].sts_status | This is for Ship-to-Store orders only. If the store is not participating in Ship-to-Store, this will always read “N/A”. |
carts[].items[].qty | Quantity of this item. |
carts[].items[].product_id | The Locally Product ID of the item. |
carts[].items[].order_resolution | The order resolution, SUCCESS , DECLINED , etc. This gives more detail as to why/how the item status is what it is. The full list of order resolution statuses can be found later in this document under Other API Functionality: Order Resolution. |
carts[].items[].upc | The UPC/EAN of the item. |
carts[].items[].alias | Array of UPC/EAN aliases or empty array in case of no aliases |
Reference Doc: GET /carts
GET /cart/{cart_hash}
endpoint
/cart/{cart_hash}
endpointThis will give you all the data for this specific cart. Your company must have access to this cart in order to retrieve its data.
To access this single cart’s information, use this endpoint:
GET https://www.locally.com/api/v2/cart/{cart_hash}
You will need to pass along your API Key in the header of the request. For example, in JSON:
{
"url": "<https://www.locally.com/api/v2/cart/QE3QL6">,
"raw_url": "<https://www.locally.com/api/v2/cart/PDQE89">,
"method": "get",
"headers": {
"Locally-API-Token": "ea432b8935xxx459be4gf147b33ae3uf0cae2337"
}
}
Sample payload response:
{
"status": true,
"properties": {
"date_time": "2022-02-03 11:11:18",
"type": "HOLD",
"hash": "QE3QL6",
"pickup_window": "2022-02-04 11:00:00",
"ordered_at": "2022-02-03 11:07:42",
"updated_at": "2022-02-03 11:09:43",
"delivery_status": "",
"invoice": "https://www.locally.com/order/QE3QL6/S615W9CDQK7M?print=1"
},
"customer": {
"first_name": "John",
"last_name": "Doe",
"address_1": "509 N Carrollton Ave",
"address_2": "",
"city": "New Orleans",
"state": "LA",
"zip": "21541",
"country": "US",
"email": "retailers@locally.com",
"phone": "",
"marketing_active_consent": "answer_yes"
},
"store": {
"name": "Locally Test Store Netherlands",
"phone": "",
"address": "1012 KN",
"address_2": "",
"city": "Amsterdam",
"state": "",
"zip": "9V9R+RV",
"country": "NL",
"store_id": 245728,
"vendor_id": 0
},
"items": [
{
"status": "Ordered",
"sts_status": false,
"product_id": 967586,
"qty": 1,
"brand": "Trek",
"upc": "601842495575",
"alias": [
"D1447020122-00003"
],
"name": "Bontrager Circuit Road Cycling Shoe",
"attrib_1": "US Men 4/Women 5.5/EU 37",
"attrib_2": "",
"currency": "EUR",
"msrp": "100.00",
"product_price": "100.00",
"product_price_less_vat": "100.00",
"subtotal": "100.00",
"tax": "0.00",
"tax_rate": "0.00",
"tax_is_vat": true,
"tax_remitted": false,
"commission": "3.50",
"stripe_fee": "3.20",
"total": "100.00",
"disbursement": "96.80",
"order_resolution": "",
"resolution_notes": "",
"messages": [
{
"sender": "retailer",
"description": "test message to customer",
"created_at": "2022-02-03 11:09:41"
},
{
"sender": "customer",
"description": "test message to store",
"created_at": "2022-02-03 11:58:21"
}
]
}
]
}
Definition of the response payload:
Field | Description |
---|---|
status | Boolean. true if request was a success, false if not. |
message | Only set if status was false, gives the reason for the false status. |
properties.date_time | The datetime of this request. |
properties.type | HOLD for a reservation for in-store payment.ORDER for a payment for in-store pickup.SHIP for a Ship-to-Store order (This is a special case for select brands only, opt-in by the retailer is required). |
properties.hash | The hash for the order. Ex. QE3QL6 |
properties.pickup_window | The datetime string of the selected pickup window slot time or empty string. |
properties.ordered_at | The datetime of when the order was placed. |
properties.updated_at | The datetime of when the order has changed last. |
properties.delivery_status | If the cart is a Same-Day Delivery, this is its current status. PushCart does not support Same-Day Delivery at this time, so this will be blank. |
properties.invoice | URL to a minimal PDF of the invoice. |
customer | Customer information |
customer.first_name | The customer’s first name |
customer.last_name | The customer’s last name |
customer.address_1 | If this is a BOPIS order or if the customer has an address on file, their billing street address. Otherwise blank. |
customer.address_2 | If this is a BOPIS order or if the customer has an address on file, their secondary billing address. Otherwise blank. |
customer.city | If this is a BOPIS order or if the customer has an address on file, their billing city. Otherwise blank. |
customer.state | If this is a BOPIS order or if the customer has an address on file, their billing state. Otherwise blank. |
customer.zip | If this is a BOPIS order or if the customer has an address on file, their billing postal code. Otherwise blank. |
customer.country | If this is a BOPIS order or if the customer has an address on file, their billing country. Otherwise blank. |
customer.email | The customers email address |
customer.phone | The customers phone number |
customer.marketing_active_consent | Whether a shopper has opted in to receive marketing emails from the store |
store | Store information |
store.name | The store’s name |
store.phone | The store’s phone number |
store.address | The store’s street address |
store.address_2 | If available, the store’s address 2 information |
store.city | The store’s city |
store.state | The store’s state |
store.zip | The store’s postal code |
store.country | The store’s country |
store.store_id | The unique Locally Store ID |
store.vendor_id | The brand's unique ID for the store, if applicable |
items | Array of items in the cart |
items[].status | The status of the item:Ordered : The BOPIS or ROPIS request has been placed and has not been resolved by the store.Confirmed : The store has confirmed that the item is in stock and is being held for the shopper.Canceled : The store has rejected the BOPIS or ROPIS request.Charge_Failed : There was an issue charging the shopper’s credit card on a BOPIS order and payment was not captured. This is rare but should be accounted for. The store should treat these like reservation requests and collect payment when the shopper arrives.sts_status : This is for Ship-to-Store orders only. If the store is not participating in Ship-to-Store, this will always read false. |
items[].product_id | The Locally product ID of the item |
items[].qty | Quantity of this item in the cart |
items[].brand | The brand / manufacturer name of the product |
items[].upc | The UPC/EAN of the item |
items[].alias | Array of UPC/EAN aliases or empty array in case of no aliases |
items[].name | The product name |
items[].attrib_1 | The product’s first attribute: color, size, etc. |
items[].attrib_2 | The product's second attribute: color, size, etc. |
items[].currency | The currency the order is in |
items[].msrp | The MSRP for the item |
items[].product_price | The item price for which the shopper paid or reserved the item. This can vary from MSRP if the store uses in-store pricing |
items[].product_price_less_vat | If the price includes VAT, the product price less VAT. Please note that this is separate from US and Canadian taxes and applies to VAT countries only. |
items[].subtotal | The subtotal of the item. |
items[].tax | The calculated tax for the item if this is a BOPIS order. |
items[].tax_rate | The tax rate for the item if this is a BOPIS order. For ex., a 6.25% tax rate would appear as 6.25. |
items[].tax_is_vat | Boolean. Whether the tax amount is VAT or not. |
items[].tax_remitted | Whether or not we remitted sales tax on behalf of the store. Locally remits sales tax on behalf of stores in the US and Canada for BOPIS orders only. |
items[].commission | The commission Locally is owed for the item. |
items[].stripe_fee | If this is a BOPIS order, the fee that Stripe charged for the transaction. |
items[].total | The total amount for the item. If this is a BOPIS order, this includes tax. |
items[].disbursement_brand | For Ship-to-store orders only, the revenue split amount payable to the brand on this order (will display as $0 until sts_status = ‘shipped’ ) |
items[].disbursement_retail | For Ship-to-store orders only, the revenue split amount payable to the retailer on this order (will display as $0 until sts_status = ‘picked up’ ) |
items[].split_brand | For Ship-to-store orders only, the brand’s revenue split amount on the order |
items[].split_retailer | For Ship-to-store orders only, the retailer’s revenue split amount on the order |
items[].order_resolution | The order resolution, SUCCESS , DECLINED , etc. This gives more detail as to why/how the item status is what it is. The full list of order resolution statuses can be found later in this document under Other API Functionality: Order Resolution. |
items[].resolution_notes | Notes regarding the order resolution if the store has included them |
items[].messages | Array of messages for the item, if any exist |
items[].messages[].sender | Whether the message was sent by the retailer or the customer |
items[].messages[].description | The contents of the message |
items[].messages[].created_at | When the message was sent |
Reference Doc: /cart/{cart_hash}
POST /cart/{cart_hash}/{upc}
endpoint
/cart/{cart_hash}/{upc}
endpointUse this endpoint to make updates to existing orders. See “Lifecycle of a PushCart Transaction” for details on the timing of these requests.
You will need to pass along your API Key in the header of the request. For example, in JSON:
{
"url": "<https://www.locally.com/api/v2/cart/QE3QL6/190340561726">,
"raw_url": "<https://www.locally.com/api/v2/cart/QE3QL6/190340561726">,
"method": "post",
"headers": {
"Locally-API-Token": "ea432b8axxxef459be46f147577e37fgcae2u37"
},
"data": {
{
"status" : "confirm"
}
}
}
Payloads:
Confirm the BOPIS or ROPIS order:
{
"status" : "confirm"
}
Reject the BOPIS or ROPIS order:
{
"status" : "reject"
}
Send a message to the shopper via SMS and email:
{
"message" : "Message goes here that will be sent to the customer via SMS and email"
}
Update the order status and include a message in the same payload:
{
"status" : "confirm",
"message" : "Looking forward to seeing you!"
}
Refund a previously confirmed BOPIS order
{
"refund":true
}
Sample response:
{
"status": true,
"debug": "Inputs: array ('status' => 'reject')",
"code": 102
}
Definition of the response payload:
Field | Description |
---|---|
status | Boolean. true if request was a success, false if not. |
message | Only set if status was false , gives the reason for the false status. |
debug | List of inputs in case debugging is needed. |
code | HTTP response code. |
Lifecycle of a PushCart Transaction
What follows is a description of the workflow, starting with the initial customer engagement in the “Buy It Locally” system through to fulfillment (pickup) of the product from the local retailer:
Payment, Communications, and Order Handoff
This section requires the use of webhooks. Read more about Locally's webhook configuration here.
1. PENDING STORE ACTION. WINDOW FOR CONFIRMATION:
Customer submits the order. If this is for a BOPIS order, their credit card is authorized for the full MSRP amount but Locally does not charge their card yet.
Locally pushes information for the new order by sending a PUT request to the webhook address you provided. The payload is as follows:
{
"upc":"601842495575",
"order_id":"QE3QL6",
"status":"ordered",
"order_url":"https://www.locally.com/api/v2/cart/QE3QL6"
}
The retailer has a 4 hour window, during store operating hours, to respond to the order via the Cart API or via the Locally notification link. If the order is placed when the store is closed, the 4 hour window begins when the store next opens.
If the 4 hour window expires, the customer will be notified via email and SMS that it has expired and that the order is canceled. Locally will also PUT an API request to your webhook endpoint about this order, indicating that it was automatically rejected. The payload is as follows:
{
"upc":"601842495575",
"order_id":"E3GKY8",
"status":"canceled",
"order_url":"https://www.locally.com/api/v2/cart/QE3QL6"
}
2. RETAILER APPROVED OR REJECTED:
You must POST to Locally’s cart
API with the status confirm
in order to approve the customer order. To reject the order, you must submit the status reject
. If the order was approved on the Locally platform, we will notify you via a PUT request to the webhook address you provided. The payload is as follows:
{
"upc":"601842495575",
"order_id":"QE3QL6",
"status":"confirmed",
"order_url":"https://www.locally.com/api/v2/cart/QE3QL6"
}
If you reject the customer’s request, we will notify the customer about the rejection.
After a BOPIS or ROPIS order has been confirmed or rejected, you can still communicate with the customer about this order through the messaging system.
If you approve a BOPIS request, the customer’s credit card will be charged immediately for the full amount and the funds will be transferred directly to your Stripe account.
Other API Functionality
SEND MESSAGE:
You can pass along any message through the POST API endpoint. For example:
{
"message":"Message goes here that will be sent to the customer via SMS and email"
}
ORDER RESOLUTION:
If not supplied, it will be assumed that the order was successful. These can be set to provide information on what happened with the order. Order resolution statuses affect your billing, so it’s important for the store to be accurate in reporting what happened.
Here are the possible order resolutions you can change a line item to:
{
"1": {
"id": 1,
"name": "The shopper purchased this item (and this item only)",
"brief_name": "purchased"
},
"2": {
"id": 2,
"name": "The shopper purchased this item and additional products",
"brief_name": "purchased +"
},
"3": {
"id": 3,
"name": "The shopper showed up, but purchased a different product than this.",
"brief_name": "purchased other"
},
"4": {
"id": 4,
"name": "The shopper showed up but did not purchase anything",
"brief_name": "no purchase"
},
"5": {
"id": 5,
"name": "No sale/the shopper did not show up to the store",
"brief_name": "no show"
}
}
Payload needed to push to us is:
{
"order_resolution":1
}
REFUND ITEM:
This can be done through the same POST API endpoint. Our system will do a full refund for the line item you are altering. Refunds will be processed immediately, though the shopper’s financial institution will generally take 5-10 business days to process the returned funds. Payload example:
{
"refund":true
}
Testing endpoints and integrations
Our current process is to flag a company as being in “test mode.” What that does is make it so if an order is placed that would normally collect money, it would use our test payment credentials. This means orders can be placed on production with a test CC (acct: 4242 4242 4242 4242 exp: 02/26, csv: 111) and it will work the same as a live order. We could create a test store that could be used as well. Please reach out to us when ready for testing.
Important Considerations
SALES TAX COLLECTION IN THE US AND CANADA:
As a Marketplace Facilitator, Locally is required to collect and remit sales tax on BOPIS orders in the US and Canada. The sales tax amount on BOPIS sales is not remitted to retailers; Locally collects the tax amount during the transaction and remits it to state and local jurisdictions. A log of the sales tax Locally has collected and remitted is available in a retailer’s Locally dashboard.
Locally uses Avalara/Avatax to calculate sales tax amount based on the item being purchased and the location of the store.
The opposite is true outside of the US and Canada. In Asia and Europe, Locally passes the tax (VAT) amount to the retailer, and it is the retailer’s responsibility to remit.
CHARGE FAILED:
While very rare, there can be cases where a BOPIS order is able to be placed, but the credit card charge fails when the retailer confirms the item. This is more common in Europe where varying authentication requirements by different banks can lead to complications, however users in the US and Canada should take this into account as well.
In this case, the item status changes to Charge_Failed
. At this point, both the shopper and the retailer are emailed informing them that the order was confirmed but the credit card charge failed and the order must be paid in-store, and the Locally order screen UI is also updated with this messaging. Pushcart users must take this into account; our recommendation is to change the type to a ROPIS order to indicate payment was not captured.
Here is a sample payload response for a failed charge:
{
"status": true,
"properties": {
"date_time": "2022-04-29 11:46:25",
"type": "ORDER",
"hash": "00001",
"pickup_window": "2022-04-29 18:00:00",
"ordered_at": "2022-04-28 11:20:48",
"updated_at": "2022-04-28 11:20:49",
"delivery_status": "",
"invoice": "https://www.locally.com/order/00001/SGZ48JH1WNOK?print=1"
},
"customer": {
"first_name": "John",
"last_name": "Smith",
"address_1": "123 Fake St",
"address_2": "",
"city": "New Orleans",
"state": "LA",
"zip": "70179",
"country": "US",
"email": "sampleshopper@gmail.com",
"phone": "+11234567890",
"marketing_active_consent": "answer_yes"
},
"store": {
"name": "Sample Store",
"phone": "+11234567890",
"address": "789 Fake St",
"address_2": "",
"city": "New Orleans",
"state": "LA",
"zip": "70471",
"country": "US",
"store_id": 12345,
"vendor_id": 0
},
"items": [
{
"status": "Charge_Failed",
"sts_status": false,
"product_id": 653104,
"qty": 1,
"brand": "Arc'teryx",
"upc": "686487512122",
"name": "Mantis 26 Backpack",
"attrib_1": "",
"attrib_2": "",
"currency": "GBP",
"msrp": "90.00",
"product_price": "90.00",
"product_price_less_vat": "75.00",
"subtotal": "90.00",
"tax": "15.00",
"tax_rate": "20.00",
"tax_is_vat": true,
"tax_remitted": false,
"shipping_cost": "0.00",
"commission": "2.63",
"stripe_fee": "0.00",
"total": "90.00",
"order_resolution": "",
"resolution_notes": "",
"messages": \[],
"disbursement": "90.00"
}
]
}