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

# Quickstart

> From npm install to your first signed webhook, on your own laptop.

Takes about 10 minutes · you need Node 20 and a dashboard account.

Every snippet on this page is extracted from the finished merchant in [`examples/saas`](https://github.com/furqaannabi/elapse/tree/master/examples/saas), so it runs as written. If you would rather read code than follow steps, clone that folder and run `npm start`.

<Steps>
  <Step title="Get a test secret key">
    Open the dashboard, go to **Developers → API keys**, and create a key. It is shown once. Keep it on the server; the SDK refuses to construct in a browser.

    Then set a payout address under **Settings**. Checkout links cannot be created until Elapse knows where to send your settlements, and live keys are issued only after it is set.

    ```bash theme={"system"}
    export ELAPSE_SECRET_KEY=sk_test_…
    export ELAPSE_API_URL=https://api.elapse.finance
    ```

    An `export` lasts only as long as the terminal. Put both in your project's `.env` (git-ignored) so your server keeps them across restarts, and in your host's environment settings when you deploy. Every snippet below reads them from the environment, and so does the CLI.
  </Step>

  <Step title="Install the SDK and construct a client">
    <Tabs>
      <Tab title="TypeScript">
        ```bash theme={"system"}
        npm install @elapse/sdk
        ```

        ```ts server.ts theme={"system"}
        const elapse = new Elapse({ secretKey, baseUrl: apiUrl });
        ```

        `secretKey` and `apiUrl` come from the environment. The hosted API is `https://api.elapse.finance`; pass it as `baseUrl` explicitly, see [Authentication](/api-reference/authentication) for why.
      </Tab>

      <Tab title="cURL">
        ```bash theme={"system"}
        curl "$ELAPSE_API_URL/v1/products" \
          -H "Authorization: Bearer $ELAPSE_SECRET_KEY"
        ```

        An empty list back means the key and the URL are right.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Create a Product">
    A Product is something billed at a rate per second. The rate is a decimal string in USD, never a float.

    <Tabs>
      <Tab title="TypeScript">
        ```ts server.ts theme={"system"}
        // Reuse a Product only if it allows pausing: the meter renders Pause from that flag, and there is
        // no products.update, so a Product created without it is left alone and a new one made beside it.
        const existing = (await elapse.products.list({ limit: 100 })).data.find((p) => p.name === PRODUCT.name && p.active && p.allow_pause);
        const product = existing ?? (await elapse.products.create({ name: PRODUCT.name, rateUsdPerSecond: PRODUCT.rateUsdPerSecond, allowPause: true }));
        ```

        `PRODUCT` is `{ name: "GPU · 4090", rateUsdPerSecond: "0.004" }`. The second line is the create call; the first looks for a Product with the same name, so restarting your server does not litter the dashboard.
      </Tab>

      <Tab title="cURL">
        ```bash theme={"system"}
        curl "$ELAPSE_API_URL/v1/products" \
          -H "Authorization: Bearer $ELAPSE_SECRET_KEY" \
          -H "Content-Type: application/json" \
          -d '{"name":"GPU · 4090","rate_usd_per_second":"0.004"}'
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Create a Checkout session on your server">
    <Tabs>
      <Tab title="TypeScript">
        ```ts server.ts theme={"system"}
        const createSession = async () => {
          const session = await elapse.checkout.sessions.create({ product: product.id, successUrl: `${baseUrl}/ok`, cancelUrl: `${baseUrl}/cancel` });
          return { id: session.id };
        };
        ```

        Pass `session.id` to your page — that is all the browser needs, and the only Elapse id it should ever hold. When the meter stops, Elapse returns the subscriber to `success_url` with `?session_id=cs_…` appended.
      </Tab>

      <Tab title="cURL">
        ```bash theme={"system"}
        curl "$ELAPSE_API_URL/v1/checkout/sessions" \
          -H "Authorization: Bearer $ELAPSE_SECRET_KEY" \
          -H "Content-Type: application/json" \
          -d '{"product":"prod_…","success_url":"http://localhost:3000/ok","cancel_url":"http://localhost:3000/cancel"}'
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Handle subscription.canceled">
    Read the raw request body, verify it, answer 200, then do your work. Never parse the body before verifying; the signature covers the exact bytes.

    <Tabs>
      <Tab title="TypeScript">
        ```ts webhooks.ts theme={"system"}
        let event;
        try {
          event = constructEvent(rawBody, signature, secret);
        } catch (err) {
          log(`✗ rejected: ${(err as Error).message}`);
          return { status: 400, body: JSON.stringify({ error: "invalid signature" }) };
        }
        ```

        ```ts webhooks.ts theme={"system"}
        return {
          status: 200,
          body: JSON.stringify({ received: true }),
          work: () => {
            if (!entitlements.first(event.id)) return log(`↺ duplicate ${event.id}`);
            const action = entitlements.apply(event);
            log(`${event.id}  ${event.type.padEnd(24)}→ ${action}`);
            if (deps.logJson !== false) log(JSON.stringify(event, null, 2));
          },
        };
        ```

        `constructEvent` throws on a missing, malformed, expired or mismatched signature. Anything it returns is genuine. The full handler, with the entitlement map and the six actions, is 45 lines in [`webhooks.ts`](https://github.com/furqaannabi/elapse/blob/master/examples/saas/src/webhooks.ts).
      </Tab>

      <Tab title="cURL">
        ```bash theme={"system"}
        # Verify by hand: sign "{t}.{raw_body}" with your whsec_ secret and compare to v1.
        printf '%s.%s' "$T" "$RAW_BODY" | openssl dgst -sha256 -hmac "$ELAPSE_WEBHOOK_SECRET"
        ```

        The [Signatures](/webhooks/signatures) page has a published vector to check against.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Forward webhooks to your laptop">
    Your server is on `localhost`, which Elapse cannot reach. The CLI opens a stream and forwards each delivery, byte for byte, with the platform's real signature.

    ```bash theme={"system"}
    npx @elapse/cli listen --forward localhost:3000/webhooks
    ```

    `npx` fetches the CLI from npm on first run; there is nothing to install. The first line it prints is your signing secret. Put it in `ELAPSE_WEBHOOK_SECRET` and restart your server. Then you will see:

    <Note>
      The CLI registers a webhook endpoint for you; it appears on the dashboard under Developers → Webhooks as `cli://`. In production you add your own `https://` URL there instead, copy the secret it shows once into your server's environment, and Elapse delivers straight to it with retries. Your server code is the same in both cases.
    </Note>

    ```text theme={"system"}
    Elapse CLI 0.1.5 · test mode · merchant Acme GPU
    Your webhook signing secret is whsec_mock000000000000000000000000000  (put it in ELAPSE_WEBHOOK_SECRET)
    Ready. Forwarding to http://localhost:3000/webhooks

    14:02:26  evt_2b     subscription.canceled      → 200 OK (8 ms)
              X-Elapse-Signature: t=1700000000,v1=5f1c…e9a2
              {
                "id": "evt_2b",
                "object": "event",
                "type": "subscription.canceled",
                "created": 1756800146,
                "livemode": false,
                "pending_webhooks": 1,
                "data": {
                  "object": {
                    "id": "sub_4QeABC",
                    "object": "subscription",
                    "status": "canceled",
                    "customer": "cus_7HaXYZ",
                    "product": "prod_9f2K",
                    "rate_usd_per_second": "0.004",
                    "seconds_elapsed": 83,
                    "amount_settled": "0.332",
                    "ended_reason": "canceled",
                    "description": "GPU · 4090  (2 spaces)"
                  }
                }
              }
    ```
  </Step>

  <Step title="Render the meter, then authorise on your phone">
    `@elapse/react` takes the session id and your publishable key. Face ID happens in a frame Elapse
    opens over your page, so your code never touches the subscriber's wallet.

    ```tsx mount.tsx theme={"system"}
    /** Ask your own server, which holds the secret key. A refusal throws; that is how `<Meter>` knows. */
    const askAcme = (what: "pause" | "resume", session: string) => async () => {
      const res = await fetch(`/${what}`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ session }),
      });
      if (res.status !== 202) throw new Error(`Acme answered ${res.status}`);
    };

    function Checkout({ session }: { session: string }) {
      const [started, setStarted] = useState(false);
      return started ? (
        <Meter
          session={session}
          proof
          onPauseRequest={askAcme("pause", session)}
          onResumeRequest={askAcme("resume", session)}
          onStopped={(e) => console.log("meter stopped", e.txHash)}
        />
      ) : (
        <Authorize
          session={session}
          onStarted={() => setStarted(true)}
          onAuthorised={() => setStarted(true)}
          onError={(e) => console.error(e.message)}
        />
      );
    }
    ```

    Open your page on a phone. Choose how long the meter may run, confirm with Face ID, watch the
    amount tick, press **Stop** after a few seconds. Your terminal shows `subscription.canceled`
    with `seconds_elapsed` and what was paid.

    ```text theme={"system"}
    14:02:26  evt_1S2b…  subscription.canceled   → revoke access · 83s · $0.33
    ```

    The docked capsule, hiding controls, and the proof drop are on the [React page](/sdks/react).
  </Step>
</Steps>

## Next

<CardGroup cols={2}>
  <Card title="Webhooks catalog" icon="bell" href="/webhooks/events">
    The other five events and what to do with each.
  </Card>

  <Card title="Clone the finished merchant" icon="github" href="https://github.com/furqaannabi/elapse/tree/master/examples/saas">
    `examples/saas`: the code these snippets come from, with a fake product page and a demo check.
  </Card>
</CardGroup>
