> ## Documentation Index
> Fetch the complete documentation index at: https://docs.macropay.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate Macropay with Encore

> Wire Macropay's merchant-of-record checkout and webhooks into an Encore TypeScript backend, with traces you can debug end-to-end.

[Encore](https://encore.dev) is an open-source TypeScript backend framework that turns your code into infrastructure: declare services, databases, and APIs, and Encore provisions and operates them for you, locally and in the cloud. It also captures distributed traces out of the box.

That tracing pairs unusually well with a billing integration. Because both your services and your Macropay calls show up in the same trace, you (or an AI coding assistant reading the code and the traces together) can follow a single payment from the `/checkout` redirect, through the customer's purchase, to the webhook that lands back in your handler — and see exactly where anything went sideways.

And because Macropay is your **merchant of record**, that's all you have to build. We're the legal seller on the buyer's statement, so we calculate and remit sales tax and VAT worldwide, keep PCI scope off your servers (cards are tokenized in our PCI DSS Level 1 compliant vault), and own chargebacks. You ship the Encore endpoints; we handle the global money plumbing.

<Tip>
  Build against the [Macropay Sandbox](https://docs.macropay.ai) first. It mirrors production behavior — checkouts, orders, webhooks — without touching real money or live data.
</Tip>

## What you'll build

| Endpoint                  | Encore pattern | Purpose                                                      |
| ------------------------- | -------------- | ------------------------------------------------------------ |
| `GET /checkout`           | `api.raw()`    | Create a checkout session and redirect the buyer             |
| `POST /webhooks/macropay` | `api.raw()`    | Verify and process events (orders, subscriptions, customers) |

Both use `api.raw()` so you control the raw HTTP request and response — required for redirects and for webhook signature verification, which depends on the exact bytes of the request body.

## Install the SDK

Pull in the Macropay JavaScript SDK alongside the Standard Webhooks verification library:

```bash Terminal theme={null}
npm install @macropayments/sdk standardwebhooks
```

## Store your secrets

Encore ships a secrets manager that behaves the same in local development and in the cloud, so you never hardcode credentials. You'll need three values.

### Access token and mode

Create an [organization access token in your organization settings](/integrate/oat) — this authenticates every API call. The mode flag tells the SDK whether to hit the sandbox or production environment.

```bash Terminal theme={null}
encore secret set --type local MACROPAY_ACCESS_TOKEN
encore secret set --type local MACROPAY_MODE
```

Set `MACROPAY_MODE` to `sandbox` while developing, and flip it to `production` when you go live.

### Webhook secret

You'll generate this when you register the webhook endpoint (see [Handle webhooks](#handle-webhooks)). For now, reserve the slot:

```bash Terminal theme={null}
encore secret set --type local MACROPAY_WEBHOOK_SECRET
```

<Note>
  These commands set **local** secrets. For Encore Cloud, set the production values in the Encore dashboard or your CI/CD pipeline — covered under [Deploy to production](#deploy-to-production).
</Note>

## Initialize the client

Read the token and mode from Encore's secret manager and construct a single shared `Macropay` instance you can import anywhere in your backend.

```tsx title="payments/macropay.ts" theme={null}
import { Macropay } from "@macropayments/sdk";
import { secret } from "encore.dev/config";

const MACROPAY_MODE = secret("MACROPAY_MODE");
const MACROPAY_ACCESS_TOKEN = secret("MACROPAY_ACCESS_TOKEN");

export const macropay = new Macropay({
  accessToken: MACROPAY_ACCESS_TOKEN(),
  server: MACROPAY_MODE() as "sandbox" | "production",
});
```

## Create a checkout endpoint

This endpoint takes a product ID from the query string, opens a hosted checkout session, and `302`-redirects the buyer to it. The same code works for a one-time purchase or a subscription — in Macropay both are just "products," so the API call doesn't change.

```tsx title="payments/payments.ts" theme={null}
import { api } from "encore.dev/api";
import { macropay } from "./macropay";

export const checkout = api.raw(
  { expose: true, path: "/checkout", method: "GET" },
  async (req, resp) => {
    const url = new URL(req.url!, `http://${req.headers.host}`);
    const productId = url.searchParams.get("product");

    if (!productId) {
      resp.writeHead(400);
      resp.end("Missing product query parameter");
      return;
    }

    try {
      const result = await macropay.checkouts.create({
        productId,
        successUrl: "https://your-app.com/welcome?checkout_id={CHECKOUT_ID}",
      });

      resp.writeHead(302, { Location: result.url });
      resp.end();
    } catch (error) {
      console.error("[Macropay] Checkout error:", error);
      resp.writeHead(500);
      resp.end("Failed to create checkout session");
    }
  }
);
```

**Try it:** open `/checkout?product=YOUR_PRODUCT_ID`. Macropay interpolates the real session ID into `{CHECKOUT_ID}` on the `successUrl`, so your post-purchase page can look the order up.

## Handle webhooks

Macropay emits events as things happen in your organization — an order is created, a subscription activates or cancels, a customer's state changes. Listening to these is how you keep your own database in sync, rather than polling the API.

### Register the endpoint

In your organization settings, open the webhooks page and click **Add Endpoint**, then:

<Steps>
  <Step title="Set the URL">
    Point the endpoint at `https://your-app.com/webhooks/macropay` — an absolute URL Macropay can reach. During local development the Macropay CLI tunnel (below) supplies this for you.
  </Step>

  <Step title="Pick your events">
    Subscribe only to what you act on. See the full catalog in the [webhook events reference](/api-reference#webhooks).
  </Step>

  <Step title="Generate a signing secret">
    Macropay signs every delivery with this secret so you can prove the request really came from us. Copy it.
  </Step>

  <Step title="Store it in Encore">
    ```bash Terminal theme={null}
    encore secret set --type local MACROPAY_WEBHOOK_SECRET
    ```
  </Step>
</Steps>

### Tunnel events to localhost

Encore runs your full app locally, so you just need to forward Macropay's events to it. The [Macropay CLI](https://docs.macropay.ai/integrate/webhooks/locally) tunnels deliveries straight to your machine:

```bash Terminal theme={null}
macropay listen http://localhost:4000/webhooks/macropay
```

The CLI prints a webhook secret on startup — store that one for local testing:

```bash Terminal theme={null}
encore secret set --type local MACROPAY_WEBHOOK_SECRET
```

### Write the handler

Use `api.raw()` so you can read the unparsed request body — signature verification runs against the exact bytes that were signed, so any reframing would invalidate it.

```tsx title="payments/payments.ts" theme={null}
import { api } from "encore.dev/api";
import { secret } from "encore.dev/config";
import { Webhook } from "standardwebhooks";

const MACROPAY_WEBHOOK_SECRET = secret("MACROPAY_WEBHOOK_SECRET");

export const webhooks = api.raw(
  { expose: true, path: "/webhooks/macropay", method: "POST" },
  async (req, resp) => {
    // Collect the raw request body
    const chunks: Buffer[] = [];
    for await (const chunk of req) {
      chunks.push(chunk);
    }
    const body = Buffer.concat(chunks).toString("utf-8");

    // Flatten headers into the shape standardwebhooks expects
    const headers: Record<string, string> = {};
    for (const [key, value] of Object.entries(req.headers)) {
      headers[key] = Array.isArray(value) ? value[0] : (value || "");
    }

    // Verify the signature before trusting anything
    let payload: any;
    try {
      const base64Secret = Buffer.from(
        MACROPAY_WEBHOOK_SECRET().trim(),
        "utf-8"
      ).toString("base64");
      const wh = new Webhook(base64Secret);
      payload = wh.verify(body, headers);
    } catch (error: any) {
      console.error("[Macropay] Invalid webhook signature:", error?.message);
      resp.writeHead(403);
      resp.end(JSON.stringify({ error: "Invalid signature" }));
      return;
    }

    // Route the verified event
    switch (payload.type) {
      case "order.created":
        console.log("[Macropay] Order created:", payload.data.id);
        break;
      case "subscription.active":
        console.log("[Macropay] Subscription active:", payload.data.id);
        break;
      case "subscription.canceled":
        console.log("[Macropay] Subscription canceled:", payload.data.id);
        break;
      case "customer.state_changed":
        console.log("[Macropay] Customer state changed:", payload.data.id);
        break;
      default:
        console.log("[Macropay] Unhandled event:", payload.type);
    }

    resp.writeHead(200);
    resp.end(JSON.stringify({ received: true }));
  }
);
```

<Warning>
  `standardwebhooks` expects a base64-encoded secret. Macropay hands you a raw string (it starts with `macropay_whs_`), so base64-encode the **entire** secret before passing it to `new Webhook()` — as shown above. Skip this and verification fails on every delivery.
</Warning>

## Run it locally

```bash Terminal theme={null}
encore run
```

Encore serves a local dashboard at `localhost:9400`. For a billing integration it earns its keep:

* **Distributed traces** for every request — including the checkout redirect and each webhook delivery
* **Service-to-service calls** laid out so you can see what your code did with an event
* **Local database queries** to confirm your records updated
* **Live logs and errors** as flows run

When a subscription fails to activate or a webhook doesn't land, you open the trace instead of guessing — the request, the verification step, and your handler are all on one timeline.

## Deploy to production

Ship to Encore Cloud (or your own cloud account) with:

```bash Terminal theme={null}
git push encore
```

Encore provisions the infrastructure automatically. Before flipping the switch, run down this list:

1. Set the production `MACROPAY_ACCESS_TOKEN`, `MACROPAY_MODE`, and `MACROPAY_WEBHOOK_SECRET` in the Encore Cloud dashboard.
2. Set `MACROPAY_MODE` to `production` so the SDK targets the live API.
3. Update the webhook endpoint in Macropay to your production domain.
4. Point `successUrl` at your production confirmation page.

Once you're live, Macropay handles tax remittance, dispute management, and PCI compliance on every order those endpoints create — so the only thing left to maintain is your Encore backend.
