Orishare
Loyalty guides

Shopify and Stripe connectors

Turn verified Shopify orders and Stripe billing events into trusted loyalty events, and let customers spend store credit at Shopify checkout.

A connector links one Shopify shop or one Stripe account to an Orishare environment. The provider sends its webhooks straight to Orishare; every delivery is checked against its signature before anything is read, and the events it produces carry the strongest trust level, verified_evidence, so rules can require proof instead of a merchant assertion.

1. Install a connector

Secrets are write-only: they are stored encrypted and no response, list or audit record ever returns them.

# Shopify: the shop domain, an Admin API access token (for discount codes) and the app's
# client secret, which Shopify uses to sign webhooks.
curl -X POST "$ORISHARE_API/v1/connectors" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Idempotency-Key: shopify-connector" -H "Content-Type: application/json" \
  -d '{"provider":"shopify","external_account":"example.myshopify.com",
       "credentials":{"access_token":"shpat_..."},"webhook_secret":"<app client secret>",
       "config":{"shop_currency":"USD"}}'

# Stripe: the account id and the signing secret of the webhook endpoint (whsec_...).
curl -X POST "$ORISHARE_API/v1/connectors" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Idempotency-Key: stripe-connector" -H "Content-Type: application/json" \
  -d '{"provider":"stripe","external_account":"acct_123","webhook_secret":"whsec_...",
       "config":{"customer_mapping":{"mode":"metadata","field":"orishare_customer_id"}}}'

The response carries webhook_path, for example /v1/connectors/shopify/cin_.../webhook. Register https://api.orishare.com followed by that path as the webhook address at the provider:

  • Shopify: subscribe to orders/paid and refunds/create (JSON format). config.shop_currency is required: the shop's currency (ISO 4217), in which every discount amount is expressed. Each connector needs its own Shopify custom app: Orishare verifies a delivery by the app's signing secret, so one secret can belong to exactly one connector, in any environment, for good: a secret stays bound to its connector after it is rotated out or the connector is deleted. Create a separate custom app for your test environment, on a Shopify development store, and never reuse the production app's secret. The delivery must also name the connector's own shop.
  • Stripe: add an endpoint for invoice.paid, customer.subscription.created, customer.subscription.deleted, payment_intent.succeeded and charge.refunded.

2. Which customer an event belongs to

config.customer_mapping decides how a payload names the Orishare customer:

ModeShopifyStripe
provider_customer_id (default)the order's customer id becomes the external idthe customer id (cus_...) becomes the external id
metadata with fieldthe order's note attribute named fieldthe object's metadata[field]

A delivery whose customer cannot be found is acknowledged and recorded as unmapped_customer. A Shopify refund belongs to the customer of its order.

3. Events you can write rules on

Provider topicOrishare eventMain properties
Shopify orders/paidorder.completedorder_id, amount_minor, currency, discount_codes
Shopify refunds/createorder.refundedorder_id, refund_id, amount_minor, currency
Stripe invoice.paidsubscription.renewedinvoice_id, subscription_id, amount_minor, currency
Stripe customer.subscription.createdsubscription.startedsubscription_id, status, price_id
Stripe customer.subscription.deletedsubscription.cancelledsubscription_id, cancellation_reason
Stripe payment_intent.succeededpayment.succeededpayment_intent_id, amount_minor, currency
Stripe charge.refundedpayment.refundedcharge_id, amount_minor (total refunded so far), currency

Amounts are integer minor units (cents). Each delivery becomes at most one event: the provider's retries of the same event are acknowledged and ignored. Only what the provider signed decides what a delivery means: an orders/paid delivery whose order Shopify does not report as paid, or whose body is not an order, is acknowledged and ignored (ignored_topic_mismatch). A refund that arrives before its order is answered 503 so Shopify delivers it again later.

4. Store credit at Shopify checkout

With a store credit wallet, a customer can turn credit into a single-use Shopify discount code:

curl -X POST "$ORISHARE_API/v1/connectors/$CONNECTOR_ID/shopify/redemptions" \
  -H "Authorization: Bearer $SECRET_KEY" -H "Idempotency-Key: redeem-1" \
  -H "Content-Type: application/json" \
  -d '{"customer_id":"cust_...","currency":"credit","amount":1500}'

The credit is held at once and the response returns the code. When the orders/paid webhook of the order that used the code arrives, the hold is captured (any part the order did not use returns), also for guest checkouts. The code expires when the hold does (hold_until on the application), and a stale hold is released by POST /v1/store_credit/release_stale. The wallet must be in the shop currency.

If Shopify refuses to create the code, the credit returns immediately and the request fails with reward_unavailable. If Shopify's answer is lost, Orishare looks the code up once: found, the redemption stands; absent, the credit returns. If that lookup fails too, the response is 202 with status: "pending_confirmation" and the credit stays held; call POST /v1/connectors/{id}/shopify/redemptions/{code}/reconcile later to settle it.

A hold behind a discount code is not released by release_stale. Call POST /v1/connectors/{id}/shopify/redemptions/expire (for example hourly). For each hold that ended more than an hour ago it deactivates the code at Shopify first, then reads its usage. Shopify counts usage asynchronously, so a zero count is never final on its own: the response lists the hold under awaiting_confirmation, and the credit returns only when a read at least 24 hours later is still zero and a search of the shop's orders finds no order that used the code. A used code stays held until its paid order's webhook captures it, and a code once seen used is never refunded later, even if the discount is deleted at Shopify. If Shopify cannot be reached or answers unclearly, the hold stays and a later call tries again. Two calls at the same moment never check the same code twice.

The order search needs the read_orders scope on the custom app. Without read_all_orders Shopify searches only the last 60 days of orders, which covers the default hold durations. A code issued more than 59 days before the search is never refunded on an empty search: the expiry lists its hold under needs_reconcile, and it stays held until you settle it with POST /v1/connectors/{id}/shopify/redemptions/{code}/reconcile after checking the shop's orders.

POST /v1/connectors/{id}/shopify/discount_codes issues a plain fixed amount code, for example as a reward.

5. Operate a connector

  • GET /v1/connectors/{id}/receipts lists recent deliveries with their outcome: processed, ignored, unmapped_customer or rejected.
  • PATCH /v1/connectors/{id} changes the config or status, or replaces the credentials or the webhook secret (rotate at the provider first, then here).
  • DELETE /v1/connectors/{id} disables the connector and erases its stored secrets. Deliveries to a disabled connector are refused.

A delivery that cannot be verified (unknown connector, wrong signature, another shop, a compressed body, Stripe timestamp older than five minutes, disabled connector) is answered 401 with the same message every time.

On this page