> For the complete documentation index, see [llms.txt](https://docs.learncard.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.learncard.com/reference/partner-connect.md).

# Partner Connect SDK

> Promise-based JavaScript SDK for secure cross-origin communication between partner apps and LearnCard

{% hint style="info" %}
**The runtime types in `packages/learn-card-partner-connect-sdk/src/types.ts` and the Zod validators in `@learncard/types` (`packages/learn-card-types/src/lcn.ts`) are the source of truth.**
{% endhint %}

The Partner Connect SDK transforms complex `postMessage` communication into clean, modern Promise-based functions. It handles the entire cross-origin message lifecycle, including request queuing, origin validation, and timeout management.

### On this section

This page covers setup, configuration, and mock mode. For the rest of the SDK, see:

* [**Methods**](/reference/partner-connect/methods.md) — the full API reference: every method, its parameters, and examples
* [**Errors, Types & Migration**](/reference/partner-connect/errors-and-types.md) — error codes, advanced configuration, the migration guide, and TypeScript types

## Features

* **🔒 Secure**: Multi-layered origin validation prevents unauthorized access
* **🎯 Type-safe**: Full TypeScript support with comprehensive type definitions
* **⚡ Promise-based**: Modern async/await API eliminates callback complexity
* **🧹 Clean**: Abstracts away all postMessage implementation details
* **📦 Lightweight**: Zero runtime dependencies, \~8KB minified
* **🛡️ Robust**: Built-in timeout handling and structured error management
* **🧪 Standalone-ready**: Runs and demos on its own via automatic mock mode — no host required

## Installation

{% tabs %}
{% tab title="npm" %}

```bash
npm install @learncard/partner-connect
```

{% endtab %}

{% tab title="Bun" %}

```bash
bun add @learncard/partner-connect
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add @learncard/partner-connect
```

{% endtab %}
{% endtabs %}

## Quick Start

```typescript
import { createPartnerConnect } from '@learncard/partner-connect';

// Initialize the SDK
const learnCard = createPartnerConnect({
    hostOrigin: 'https://learncard.app',
});

// Request user identity (SSO)
try {
    const identity = await learnCard.requestIdentity();
    console.log('User DID:', identity.user.did);
    console.log('JWT Token:', identity.token);
} catch (error) {
    if (error.code === 'LC_UNAUTHENTICATED') {
        console.log('User is not logged in');
    }
}
```

## Standalone / Mock Mode

The SDK only does real work when it's embedded inside LearnCard — that's what answers its requests. Run your app on its own (local dev, a preview deploy, tests) and there's nothing to answer. Standalone calls that aren't mocked reject immediately with `LC_NOT_EMBEDDED` (instead of hanging until the request timeout), plus a one-time console hint.

Mock mode fixes this automatically in local development and AI app builders. Whenever no LearnCard host is present and your app runs on a local dev host (`localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, `*.local`) or in the editor preview of Lovable, Bolt, v0, or Replit, the SDK stands in for LearnCard so your app stays fully usable:

* **Every method shows a short notice** describing what would happen once embedded — e.g. `sendCredential` → *"In LearnCard, the learner would receive **\[name]**."*, `incrementCounter` → *"Counter **coins** → **10**."*, `launchFeature` → *"In LearnCard, this would open **/wallet**."* Strong, visible feedback for every call. At most three show at once (one on phones).
* A **Practice mode** pill in the bottom-left corner expands into a plain-language list of the LearnCard features your app uses, with a **Publish app** button and a copy-link button.

Captures include a stable `appKey` saved per app title, or the exact `mockOptions.appId` you provide, so apps sharing an address remain distinct. Use **Start over** in the expanded panel (with confirmation) or call `learnCard.resetPracticeMode()` to clear only this app's captures, practice credentials, and counters. The next capture gets a new generated identity; explicit `appId` values stay unchanged. The method is safe with the UI disabled and does nothing outside mock mode. Icon capture prefers Apple touch icons, large favicons, and same-origin web app manifest icons before `/favicon.ico`, and tries to include a small rasterized image. Long publish links omit the embedded image.

* `requestConsent(...)` grants automatically and the notice spells out what the learner would be asked to share; counters (`incrementCounter` / `getCounter` / `getCounters`) save to the browser and survive reloads.
* Identical or polled calls coalesce into one notice with a ×N counter, so nothing spams the screen.
* `requestIdentity`, notifications, learner context, and sync status return sensible placeholder data.
* Everything is also logged to the console with a `[LearnCard SDK · MOCK]` prefix.

No flags, no separate build in local dev. Your app is demo-able locally and behaves exactly the same against the real host once embedded.

{% hint style="warning" %}
**`'auto'` never mocks on a published app.** App-builder previews are recognized by the editor framing your app (e.g. `lovable.dev`) or by a preview-only address; published addresses such as `*.lovable.app`, `*.vercel.app`, `*.replit.app`, or your own domain never mock, because a real user opening your app's URL directly must never receive a fabricated identity or auto-granted consent. For other deploy previews (Netlify, Vercel, …) that should demo standalone anywhere but use the real host once embedded, opt in with `mock: 'standalone'`. For CI and tests that should always mock, use `mock: true`.
{% endhint %}

| `mock`             | Local dev or app-builder preview | Standalone, other origin      | Embedded in LearnCard |
| ------------------ | -------------------------------- | ----------------------------- | --------------------- |
| `'auto'` (default) | mock                             | fail fast (`LC_NOT_EMBEDDED`) | real host             |
| `'standalone'`     | mock                             | mock                          | real host             |
| `true`             | mock                             | mock                          | mock                  |
| `false`            | fail fast                        | fail fast                     | real host             |

If your app is embedded in something that isn't LearnCard (a cross-origin Storybook canvas, a preview shell), calls don't hang: the SDK mocks in local dev and app-builder previews and otherwise rejects fast with `LC_NOT_EMBEDDED`. When the parent can't be identified (Firefox, or a same-origin localhost wrapper), a one-time side-effect-free presence probe decides — the SDK only mocks if no host answers within `hostProbeTimeout` (default 1500 ms).

Every mocked call shows a labeled toast and a `[LearnCard SDK · MOCK]` console log, so it's clear the SDK is simulating rather than talking to a real host. For a production build meant to run only inside LearnCard, set `mock: false` — standalone calls then reject immediately with `LC_NOT_EMBEDDED`.

```typescript
// Mocks in local dev when standalone; real host when embedded in LearnCard.
const learnCard = createPartnerConnect();

await learnCard.sendCredential({ templateAlias: 'course-completion' });
```

**Overrides:**

```typescript
createPartnerConnect({ mock: 'standalone' }); // mock when no host, on any origin; real when embedded
createPartnerConnect({ mock: true }); // always mock, even embedded (CI, tests)
createPartnerConnect({ mock: false }); // never mock (standalone → LC_NOT_EMBEDDED)
createPartnerConnect({
    mockOptions: {
        ui: true, // toasts/banners (default true)
        log: true, // console logging (default true)
        persist: true, // save counters to the browser (default true)
        namespace: 'my-app-mock', // storage namespace for mock data
        // Seed data for demos / happy-path UI:
        identity: { did: 'did:web:example.com:me', name: 'Ada' },
        credentials: [{ templateAlias: 'course-completion', name: 'Algebra 101' }],
        counters: { coins: 50 },
    },
});
```

Use `learnCard.isMocked()` to check whether an instance is currently mocking.

The mock keeps a small session store, so **reads reflect writes**: after `sendCredential(...)`, calls like `checkUserHasCredential`, `getTemplateRecipients`, `requestLearnerContext`, and `askCredentialSearch` return that credential — so happy-path UI actually lights up standalone. Use `mockOptions.credentials` / `identity` / `counters` to pre-populate state without performing an action first. Mock credentials are marked `_mock: true` and are never cryptographically valid.

## Security Model

The Partner Connect SDK implements comprehensive security measures:

### Origin Validation

**Strict Enforcement:**

* Incoming messages must exactly match the configured host origin
* No wildcard (`*`) origins are ever used
* Query parameter overrides are validated against whitelist

**Configuration Hierarchy:**

1. **Default**: `https://learncard.app` (security anchor)
2. **Query Parameter Override**: `?lc_host_override=https://staging.learncard.app`
3. **Configured Origin**: From `hostOrigin` option

**Example:**

```typescript
// Production configuration
const learnCard = createPartnerConnect({
    hostOrigin: 'https://learncard.app',
});
// Uses: https://learncard.app
// Override: ?lc_host_override=X (not validated, warning logged)

// Staging configuration with whitelist
const learnCard = createPartnerConnect({
    hostOrigin: ['https://learncard.app', 'https://staging.learncard.app'],
});
// Default: https://learncard.app
// Override: ?lc_host_override=https://staging.learncard.app ✅
// Override: ?lc_host_override=https://evil.com ❌
```

### Message Security

* **Protocol Verification**: Messages must match expected protocol version
* **Request ID Tracking**: Only tracked requests are processed
* **Timeout Protection**: Requests automatically timeout to prevent hanging
* **Cleanup on Destroy**: Pending requests are properly rejected

## Browser Support

* **Chrome/Edge**: 90+
* **Firefox**: 88+
* **Safari**: 14+
* **Mobile**: iOS Safari 14+, Android Chrome 90+

**Required APIs:**

* `postMessage`
* `Promise`
* `URLSearchParams`
* `addEventListener`

## Related Documentation

* [Build an App Inside LearnCard](/build/publish-your-app.md) - Step-by-step guide for App Store credential issuance
* [LearnCard Core SDK](https://github.com/learningeconomy/LearnCard/tree/main/sdks/learncard-core/README.md) - Backend credential operations
* [LearnCard Network](https://github.com/learningeconomy/LearnCard/tree/main/sdks/learncard-network/README.md) - Network integration
* [Connect a User's LearnCard to Your Platform](/build/create-a-consentflow.md) - Integration guide
* [App Store Development](https://github.com/learningeconomy/LearnCard/tree/main/apps/learn-card-app/README.md) - LearnCard app ecosystem


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.learncard.com/reference/partner-connect.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
