# Accept crypto on Shopify with crypt.pe

This guide walks you through accepting **ETH, USDC, USDT, BTC, SOL, MATIC**
and 8 other coins on your Shopify store, with funds settling **directly to
your wallet** — crypt.pe never holds them.

There are two ways to integrate, depending on your plan:

| Plan | Integration | Cart total appears on crypt.pe checkout? | Webhook auto-flips Shopify? |
|------|-------------|------------------------------------------|-----------------------------|
| Any Shopify plan | **Manual payment method** (this guide) | ✅ | ✅ via the small middleware in §3 |
| Shopify Plus | **Custom payment app** (Partner-app required) | ✅ | ✅ (native) |

For 95 % of stores the manual-payment-method route below is what you want:
zero Partner-app paperwork, ships today, and uses the same `/v1/payments`
API our WooCommerce / Magento plugins use.

---

## 1. Get your crypt.pe credentials

1. Sign up at <https://crypt.pe/signup> and add the wallet addresses you
   want funds to land in.
2. Go to **Dashboard → Gateway → Create API key**.
3. Copy your `sk_live_…` and `whsec_…` **once** — they're never shown again.
4. Stash both in your middleware host's environment (Vercel, Cloudflare
   Workers, Railway, etc.).

## 2. Enable the manual payment method in Shopify

1. **Shopify Admin → Settings → Payments → Manual payment methods**
2. Click **Add manual payment method → Create custom payment method**
3. Name: `Pay with crypto (crypt.pe)`
4. Additional details: leave blank (we surface them on the checkout page)
5. Payment instructions: paste the snippet below — Shopify will show it on
   the order-confirmation page after checkout while we await the on-chain
   payment.

```text
Thanks! To complete your order, send the exact crypto amount shown on
your crypt.pe checkout page. Your order will be marked as paid
automatically the moment the network confirms it (usually under 2
minutes). If you closed the tab, the checkout link was emailed to you.
```

## 3. The 30-line middleware

Shopify's "manual payment method" doesn't expose an HTTP callback, so we
need a thin proxy that:

1. Listens for Shopify's `orders/create` webhook
2. Calls `POST https://crypt.pe/api/v1/payments` with the cart total
3. Sends the resulting `checkout_url` to the customer (via email and an
   admin-note that's visible in the Shopify customer view)
4. Listens for crypt.pe's `payment.confirmed` webhook and flips the
   matching Shopify order to **Paid** via Shopify's REST API.

A reference Node.js Cloudflare Worker doing exactly that:

```js
// Deploy to Cloudflare Workers, Vercel, or Railway.
// Env vars required:
//   CRYPTPE_SECRET_KEY      (sk_live_…)
//   CRYPTPE_WEBHOOK_SECRET  (whsec_…)
//   SHOPIFY_STORE           (your-store.myshopify.com)
//   SHOPIFY_ADMIN_TOKEN     (Admin API access token w/ orders write scope)

import Cryptpe from "./cryptpe.js"; // our Node SDK
const cryptpe = new Cryptpe(process.env.CRYPTPE_SECRET_KEY);

export default {
  async fetch(req, env) {
    const url = new URL(req.url);

    // ---- Shopify → us: a new order was placed
    if (url.pathname === "/shopify/orders-create" && req.method === "POST") {
      const order = await req.json();
      if (order.gateway !== "crypt.pe (manual)") {
        return new Response("ignored", { status: 200 });
      }
      const cp = await cryptpe.payments.create({
        amount_usd:     parseFloat(order.total_price),
        accepted_coins: ["eth", "usdc-erc20", "usdt-erc20", "btc", "sol"],
        return_url:     `https://${env.SHOPIFY_STORE}/account/orders/${order.id}`,
        webhook_url:    `${url.origin}/cryptpe/webhook`,
        customer_email: order.email,
        metadata:       { shopify_order_id: order.id },
      });
      // Stash the checkout URL on the Shopify order so staff can see it,
      // and email it to the customer separately.
      await shopify(env, `/admin/api/2024-10/orders/${order.id}.json`, "PUT", {
        order: { id: order.id, note: `Pay here: ${cp.checkout_url}` },
      });
      // (production: also dispatch an email with the link)
      return new Response("ok");
    }

    // ---- crypt.pe → us: payment lifecycle event
    if (url.pathname === "/cryptpe/webhook" && req.method === "POST") {
      const raw = await req.text();
      let event;
      try {
        event = cryptpe.webhooks.verify(
          raw,
          req.headers.get("x-cryptpe-signature"),
          env.CRYPTPE_WEBHOOK_SECRET,
        );
      } catch (e) {
        return new Response(`bad signature: ${e.message}`, { status: 400 });
      }
      const shopifyId = event.data.metadata?.shopify_order_id;
      if (!shopifyId) return new Response("no shopify id", { status: 200 });

      if (event.type === "payment.confirmed") {
        // Mark the Shopify order as paid.
        await shopify(env,
          `/admin/api/2024-10/orders/${shopifyId}/transactions.json`,
          "POST",
          {
            transaction: {
              kind: "capture",
              status: "success",
              amount: event.data.amount_usd,
              gateway: "crypt.pe (manual)",
            },
          });
      } else if (event.type === "payment.refunded") {
        await shopify(env, `/admin/api/2024-10/orders/${shopifyId}/refunds.json`,
          "POST",
          { refund: { notify: true, note: "crypt.pe refund recorded" } });
      } else if (event.type === "payment.cancelled") {
        await shopify(env, `/admin/api/2024-10/orders/${shopifyId}/cancel.json`,
          "POST", {});
      }
      return new Response(`ok ${event.type}`);
    }

    return new Response("not found", { status: 404 });
  },
};

async function shopify(env, path, method, body) {
  return fetch(`https://${env.SHOPIFY_STORE}${path}`, {
    method,
    headers: {
      "X-Shopify-Access-Token": env.SHOPIFY_ADMIN_TOKEN,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
}
```

## 4. Register the Shopify webhooks (one-time)

```bash
# Listen for new orders
curl -X POST "https://$SHOPIFY_STORE/admin/api/2024-10/webhooks.json" \
  -H "X-Shopify-Access-Token: $SHOPIFY_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "webhook": {
          "topic":   "orders/create",
          "address": "https://your-worker.example.com/shopify/orders-create",
          "format":  "json"
        }
      }'
```

## 5. Test end-to-end

1. From the storefront, place a $1 test order and pick *Pay with crypto*.
2. Shopify creates the order with status **Pending**.
3. The worker mints a crypt.pe order and stamps the checkout URL on the
   Shopify order note (you'll see it in admin).
4. Open the checkout URL, scan the QR, send a tiny test transfer.
5. Within seconds the worker receives `payment.confirmed`, calls
   `transactions.json`, and Shopify flips the order to **Paid**.

That's it. You're accepting crypto on Shopify, non-custodially, in any of
the 13 networks crypt.pe supports — and the funds are already in your
wallet.

Need help? Email <integrations@crypt.pe>.
