This is the reference for E-commerce checkout with PayPal: every asset with its fields, the downloads, how to adapt the shop to another payment provider, and what to check when something does not work.

Downloads

  • [shop-bundle.json](/casestudies/ecommerce/shop-bundle.json): six records, three Logic Blocks and two automations, for Import Metadata in the console.
  • [webhook-landing.json](/casestudies/ecommerce/webhook-landing.json): how PayPal's three events land. Apply it under Org Settings → Connectors → PayPal → Customize → Webhook, or with the set_connector_webhook_landing tool.
  • [checkout-cart.glyph.py](/casestudies/ecommerce/checkout-cart.glyph.py): the checkout Glyph's source. A Glyph is created in the console (Glyphs → New Glyph), not through Import Metadata.

Records and fields

Every record also has the platform's id and name fields. Types are the platform's: string, number, boolean.

RecordFieldTypeMeaning
producttitlestringDisplay name
productdescriptionstringShort description
productpricenumberUnit price in the shop's currency
productactivebooleanWhether it can be sold
cartstatusstringopen, checkout_started, payment_captured, ordered, payment_failed
cartcurrencystringThree-letter code, for example USD
cartitem_countnumberSum of the lines' quantities
cartsubtotalnumberSum of the lines' totals
cartuser_idstringThe signed-in shopper who opened it
cart_linecart_idstringThe cart this line belongs to
cart_lineproduct_idstringThe product
cart_linetitlestringProduct title at the time
cart_linequantitynumberHow many
cart_lineunit_pricenumberPrice at the time it was added
cart_lineline_totalnumberquantity times unit_price
payment_approvalcart_idstringThe cart, from the checkout reference
payment_approvalpaypal_order_idstringPayPal's order id, used to capture
payment_approvalstatusstringapproved, then captured
shop_ordercart_idstringThe cart that was paid
shop_orderstatusstringpaid
shop_orderpayment_idstringPayPal's capture id
shop_orderamountnumberAmount captured
shop_ordercurrencystringCurrency captured
shop_orderpaid_atstringWhen PayPal took the payment
shop_orderitem_countnumberTotal items, set by create-order-items
shop_order_itemorder_idstringThe order
shop_order_itemproduct_idstringThe product
shop_order_itemtitlestringProduct title
shop_order_itemquantitynumberHow many
shop_order_itemunit_pricenumberUnit price
shop_order_itemline_totalnumberLine total

Logic Blocks

Logic BlockInputSteps in shortReturns
add-to-cartproduct_id (required), quantity, cart_idget the product; if a cart id was given get that cart, otherwise create one; find an existing line for the product; raise it or add a new one; list the cart's lines and recompute item count and subtotal; get the cartcart_id, cart, items
capture-paypal-orderorder_id, approval_id, cart_id (all required)PayPal connector step Capture payment with order_id; set the approval's status to captured; set the cart's status to payment_capturedcapture
create-order-itemsorder_id (required)get the order; list the cart's lines; for each line create a shop_order_item; store the item count on the order; set the cart's status to orderedorder_id, items_copied

The capture step, as it appears in the Logic Block's JSON:

json
{
  "id": "capture",
  "type": "connector",
  "config": {
    "connectorId": "paypal",
    "actionId": "capture_order",
    "fieldMappings": [{ "field": "order_id", "value": "{{input.order_id}}" }]
  }
}

Automations

AutomationOnRunsField mapping
capture-paypal-order-on-approvalpayment_approval created (after)capture-paypal-orderorder_id = the record's paypal_order_id; approval_id = its id; cart_id = its cart_id
create-order-items-on-ordershop_order created (after)create-order-itemsorder_id = the record's id

Webhook landing

json
{
  "CHECKOUT.ORDER.APPROVED": {
    "target_entity": "payment_approval", "operation": "insert",
    "field_mapping": { "cart_id": "{{ipaas_ref.entityRef}}", "paypal_order_id": "{{resource.id}}", "status": "approved" }
  },
  "PAYMENT.CAPTURE.COMPLETED": {
    "target_entity": "shop_order", "operation": "insert",
    "field_mapping": { "cart_id": "{{ipaas_ref.entityRef}}", "status": "paid", "payment_id": "{{resource.id}}",
                       "amount": "{{resource.amount.value}}", "currency": "{{resource.amount.currency_code}}",
                       "paid_at": "{{resource.create_time}}" }
  },
  "PAYMENT.CAPTURE.DENIED": {
    "target_entity": "cart", "operation": "update", "target_id": "{{ipaas_ref.entityRef}}",
    "field_mapping": { "status": "payment_failed" }
  }
}

Using another payment provider

The records, the add-to-cart block and the order-items block do not change. You change the checkout, the landing and (for PayPal only) drop the capture step. Check each provider's action and events in Payment connectors; this study ran end to end on PayPal only.

ProviderItems in a cart lineSeparate capture step
PayPalname, quantity, unit price, plus the order totalYes: capture_order after the approval
StripeA real Stripe Price id and a quantityNo: the payment completes at checkout
PaddleA real Paddle Price id and a quantityNo
DodoA Dodo product id and a quantityNo
ProviderEvent that means paidFailure event
PayPalPAYMENT.CAPTURE.COMPLETEDPAYMENT.CAPTURE.DENIED
Stripecheckout.session.completedcheckout.session.async_payment_failed
Paddletransaction.completedtransaction.payment_failed
Dodopayment.succeededpayment.failed

All four use the checkout_cart action for a multi-item cart.

Providers with a price catalog

Stripe, Paddle and Dodo keep prices in their own catalog, so a cart line carries the provider's price id rather than a name and amount. Add a price id field to product and read it in your checkout. PayPal has no price catalog, which is why its items carry a name and a unit price.

Troubleshooting

SymptomLikely causeWhat to do
The checkout call fails with a PayPal error about the return URLA success or cancel URL has no https://Tenant Settings → Connectors → PayPal → Checkout redirect URLs: use full addresses and save
The browser reports a CORS error calling your appThe calling site is not on Allowed DomainsOrg Settings → Allowed Domains: add the site's address
The PayPal call is refused for the domainThe PayPal API domain is not allowedAdd the domains the PayPal connector card lists (api-m.sandbox.paypal.com for Test Mode)
A Logic Block or Glyph returns 'not enabled for this org'The connector is not adoptedOrg Settings → Connectors: add PayPal
No payment_approval appears after approvingThe webhook is not registered, has the wrong events, or the Webhook ID is wrongCheck the URL and the three events in PayPal, and the Webhook ID on the tenant card; the Debug Log shows webhooks that did arrive
The approval exists but the order never appearsThe capture did not run or PayPal has not sent the completed eventDebug Log: look for the capture-paypal-order run and the PayPal response; confirm PAYMENT.CAPTURE.COMPLETED is subscribed
The order exists but there are no order itemsThe create-order-items automation is off or failedAPI Studio → shop_order → Automations: check it is enabled; the Debug Log shows its run
A shopper gets 403 on create or runThe app user has the read-only policyTenant Management → App Users → ACL: give the shopper memberacl
The checkout total is rejectedThe items do not add up to the totalBuild the total from the same items you send; use two decimal places as strings