Connect any online store to email: store API and CSV import
ProfilePilot Team | Oct 6, 2026 | 7 min read
No connector for your shop? Post orders, customers, abandoned carts and products from your store, Zapier, Make or your own middleware, or upload past orders from a CSV.
What a custom store does
Use a custom store when your shop has no ready-made connection in ProfilePilot, for example a bespoke site, a marketplace or a platform that is still coming soon.
Your store (or a tool such as Zapier, Make or your own middleware) sends ProfilePilot four kinds of data:
- order_placed: an order with its total, currency and items. It starts automations that use the order_placed trigger, and first_purchase for a customer's first order.
- customer: adds or updates a customer.
- cart_abandoned: a cart the shopper left, with a link back to it. It starts automations that use the cart_abandoned trigger.
- product: adds or updates a product, ready for product blocks and segments.
These are the same events WooCommerce and Shopify stores send, so the same automations work for every store.
Who receives email
New people are added to Contacts not subscribed. They are only subscribed when you send accepts_marketing: true, which you should do only when they ticked your marketing box at checkout or sign-up.
- Anyone who unsubscribed from your emails, bounced or complained stays that way, whatever the store sends.
- accepts_marketing: false removes a subscription only if it came from your store. It never overrides a choice the contact made in ProfilePilot.
- Leaving accepts_marketing out changes nothing.
Automations still record events for people who are not subscribed, but they do not receive marketing email.
Set up the store and its key
Open Marketing → Email → Store connections and choose Set up a custom store under the platform list. Give it a name, and optionally its website and default currency.
ProfilePilot shows the store's API key once. It starts with ppem_. Copy it into your server's secrets or your middleware's connection settings.
- Never put the key in a web page, an app or source control.
- Rotate key gives you a new key and stops the old one straight away.
- Disconnect stops the key. Contacts and order history stay.
- Pause keeps the key but refuses data (409) until you resume.
Store connections are part of the paid email plans.
Send data: POST /api/v1/email/store
- URL: your ProfilePilot address followed by /api/v1/email/store (the card on the Store connections page shows the exact URL).
- Authorization: Bearer followed by a space and the store key.
- Content-Type: application/json. Bodies may be up to 64 KB.
- Rate limit: 600 requests per minute per key (429 with Retry-After above that).
The body is {"type": "...", "order" | "customer" | "cart" | "product": {...}}. Field names use snake_case. Timestamps are ISO-8601.
Example: an order
curl -sS -X POST "https://profilepilotapp.com/api/v1/email/store" \
-H "Authorization: Bearer ppem_your_store_key" \
-H "Content-Type: application/json" \
-d '{
"type": "order_placed",
"order": {
"id": "10045",
"number": "#10045",
"email": "sam@example.com",
"first_name": "Sam",
"accepts_marketing": true,
"currency": "USD",
"total": 84.5,
"placed_at": "2026-10-06T09:30:00Z",
"line_items": [{ "sku": "TEA-01", "name": "Rooibos tea", "quantity": 2, "price": 42.25 }]
}
}'
Required: id, email and total. Optional: number, status (default paid; pending, failed, cancelled and refunded are not counted as purchases), cancelled, currency, subtotal, discount, placed_at, customer_id, first_name, last_name or name, phone, accepts_marketing, line_items (up to 100: product_id, variant_id, sku, name, quantity, price) and test.
A new order returns 202 with the events it fired:
{ "ok": true, "type": "order_placed", "contactId": "…", "events": ["order_placed", "first_purchase"], "duplicate": false }
Posting the same order id again updates it and returns 200 with "duplicate": true. An order id never starts automations twice, so retrying after an error is safe.
Example: an abandoned cart
{
"type": "cart_abandoned",
"cart": {
"id": "cart-9f2",
"email": "sam@example.com",
"cart_url": "https://shop.example.com/cart/recover/9f2",
"currency": "USD",
"total": 42.25,
"line_items": [{ "sku": "TEA-01", "name": "Rooibos tea", "quantity": 1, "price": 42.25 }]
}
}
Send it when your store decides the cart is abandoned (for example an hour without checkout). cart_url must be an https link that reopens the cart; put {{event.cart_url}} on the email button. A cart id fires once, and not at all if the shopper ordered after creating the cart or the cart is more than 72 hours old.
Customers and products
{ "type": "customer", "customer": { "id": "c-778", "email": "sam@example.com", "first_name": "Sam", "accepts_marketing": false } }
{ "type": "product", "product": { "id": "p-201", "sku": "TEA-01", "name": "Rooibos tea", "url": "https://shop.example.com/p/rooibos", "image_url": "https://shop.example.com/img/rooibos.jpg", "price": 42.25, "currency": "USD", "category": "Tea", "in_stock": true } }
A customer needs email; a product needs id and name. Neither starts an automation.
Zapier, Make and other middleware
Any tool that can make an HTTP request can post the same JSON. In Zapier use Webhooks by Zapier → Custom Request; in Make use the HTTP → Make a request module.
- Method POST, the URL from the Store connections card, and two headers: Authorization (Bearer and your store key) and Content-Type (application/json).
- Map your store's order fields into the JSON body shown above.
- Keep the key in the tool's connection or secret settings, not in a shared note.
Upload past orders and customers (CSV)
On the custom store, choose API & CSV → Upload CSV. Pick Orders or Customers, choose the file and check the column matches.
- Shopify and WooCommerce order exports are recognised automatically, including files with one row per item (rows with the same order id become one order).
- For other files, use these headers: id, email, total, currency, placed_at, status, first_name, last_name, accepts_marketing, item_name, item_sku, item_quantity, item_price.
- Imported history never starts automations, and an imported order id never fires later either.
- Rows without an id, email or total are skipped and counted in the result.
Upload history before you start posting live orders, so a returning customer's next order is not mistaken for their first purchase.
HTTP status codes
- 202 / 200: saved (200 when the order or cart had already fired; read duplicate and events).
- 400: the JSON could not be used, for example a missing email or an http cart link.
- 401: the key is missing, wrong, revoked, or not a store key.
- 402: the workspace is on the Free email plan.
- 403: email is not enabled for the workspace, or x-org-id does not match the key.
- 409: the store is paused in ProfilePilot.
- 413: the body is larger than 64 KB.
- 429: too many requests; wait for Retry-After seconds.
- 500: ProfilePilot could not save it; retry with the same id.
Suggested first test
- Choose Send test event on the store. It checks the key, the store status and the example order, and lists the active automations listening for each event. It saves nothing.
- Create an automation that starts on the event order_placed for a test contact you own, and turn it on.
- Post one order_placed with a new order id for that contact. Confirm the reply lists "order_placed" and the contact enters the automation.
- Post the same request again and confirm duplicate is true.
- Turn the automation off again if it was only a test.
Quick Tasks
- ✓Open Marketing → Email → Store connections and choose Set up a custom store
- ✓Copy the store API key straight away and keep it on your server or in your middleware
- ✓Choose Send test event to check the setup
- ✓Post one order_placed for a test contact you own
- ✓Upload past orders from a CSV so first purchases and segments start with full history
Keep learning
See the latest guides, experiments, and rollouts.