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:

upcqtyprice
727602259178110.99
605284574539299.95
736745040314100147.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

Now 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:

FieldDescription
statusBoolean. true if request was a success, false if not.
messageOnly sent if status was false, gives the reason for the false status.
properties.date_timeThe datetime of this request.
properties.current_pageThe current page you are on. Pagination starts at page 0 (zero).
properties.total_pagesTotal number of pages. If total pages is 5, then pages 0, 1, 2, 3, 4 exist.
cartsArray of carts.
carts[].hashThe unique hash for the order. For example E3GKY8.
carts[].typeHOLD 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_itemsNumber of items in the order.
carts[].sourceDomain where the order originated from. “From Locally.com”, “From brand-site.com”, etc.
carts[].ordered_atThe datetime of when the order was placed.
carts[].updated_atThe datetime of when the order has changed last.
carts[].delivery_statusIf 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_windowThe datetime string of the selected pickup window slot time or empty string.
carts[].itemsBreakdown of items in carts statuses and basic information. Please note that there can be more than one item per cart.
carts[].items[].currencyThe currency the order is in. Adheres to ISO 4217
carts[].items[].totalThe total amount for the item. Includes tax if this is a "Buy Online, Pickup in Store (BOPIS)" order.
carts[].items[].statusThe 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_statusThis 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[].qtyQuantity of this item.
carts[].items[].product_idThe Locally Product ID of the item.
carts[].items[].order_resolutionThe 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[].upcThe UPC/EAN of the item.
carts[].items[].aliasArray of UPC/EAN aliases or empty array in case of no aliases

Reference Doc: GET /carts

GET /cart/{cart_hash} endpoint

This 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": "[email protected]",  
        "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:

FieldDescription
statusBoolean. true if request was a success, false if not.
messageOnly set if status was false, gives the reason for the false status.
properties.date_timeThe datetime of this request.
properties.typeHOLD 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.hashThe hash for the order. Ex. QE3QL6
properties.pickup_windowThe datetime string of the selected pickup window slot time or empty string.
properties.ordered_atThe datetime of when the order was placed.
properties.updated_atThe datetime of when the order has changed last.
properties.delivery_statusIf 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.invoiceURL to a minimal PDF of the invoice.
customerCustomer information
customer.first_nameThe customer’s first name
customer.last_nameThe customer’s last name
customer.address_1If this is a BOPIS order or if the customer has an address on file, their billing street address. Otherwise blank.
customer.address_2If this is a BOPIS order or if the customer has an address on file, their secondary billing address. Otherwise blank.
customer.cityIf this is a BOPIS order or if the customer has an address on file, their billing city. Otherwise blank.
customer.stateIf this is a BOPIS order or if the customer has an address on file, their billing state. Otherwise blank.
customer.zipIf this is a BOPIS order or if the customer has an address on file, their billing postal code. Otherwise blank.
customer.countryIf this is a BOPIS order or if the customer has an address on file, their billing country. Otherwise blank.
customer.emailThe customers email address
customer.phoneThe customers phone number
customer.marketing_active_consentWhether a shopper has opted in to receive marketing emails from the store
storeStore information
store.nameThe store’s name
store.phoneThe store’s phone number
store.addressThe store’s street address
store.address_2If available, the store’s address 2 information
store.cityThe store’s city
store.stateThe store’s state
store.zipThe store’s postal code
store.countryThe store’s country
store.store_idThe unique Locally Store ID
store.vendor_idThe brand's unique ID for the store, if applicable
itemsArray of items in the cart
items[].statusThe 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_idThe Locally product ID of the item
items[].qtyQuantity of this item in the cart
items[].brandThe brand / manufacturer name of the product
items[].upcThe UPC/EAN of the item
items[].aliasArray of UPC/EAN aliases or empty array in case of no aliases
items[].nameThe product name
items[].attrib_1The product’s first attribute: color, size, etc.
items[].attrib_2The product's second attribute: color, size, etc.
items[].currencyThe currency the order is in
items[].msrpThe MSRP for the item
items[].product_priceThe 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_vatIf 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[].subtotalThe subtotal of the item.
items[].taxThe calculated tax for the item if this is a BOPIS order.
items[].tax_rateThe 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_vatBoolean. Whether the tax amount is VAT or not.
items[].tax_remittedWhether 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[].commissionThe commission Locally is owed for the item.
items[].stripe_feeIf this is a BOPIS order, the fee that Stripe charged for the transaction.
items[].totalThe total amount for the item. If this is a BOPIS order, this includes tax.
items[].disbursement_brandFor 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_retailFor 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_brandFor Ship-to-store orders only, the brand’s revenue split amount on the order
items[].split_retailerFor Ship-to-store orders only, the retailer’s revenue split amount on the order
items[].order_resolutionThe 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_notesNotes regarding the order resolution if the store has included them
items[].messagesArray of messages for the item, if any exist
items[].messages[].senderWhether the message was sent by the retailer or the customer
items[].messages[].descriptionThe contents of the message
items[].messages[].created_atWhen the message was sent

Reference Doc: /cart/{cart_hash}

POST /cart/{cart_hash}/{upc} endpoint

Use 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:

FieldDescription
statusBoolean. true if request was a success, false if not.
messageOnly set if status was false, gives the reason for the false status.
debugList of inputs in case debugging is needed.
codeHTTP 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": "[email protected]",  
        "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"  
        }  
    ]  
}