---
title: "Partner External ID Linking"
description: "Link an authenticated AGG user to your app's internal user ID with an HMAC assertion"
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

<Info>
  This flow is for partners who need a trusted mapping between an AGG `user.id` and their own
  internal user ID for rewards, analytics, or reconciliation. The assertion must come from your
  backend, not from the browser.
</Info>

## How it works

<Steps>
  <Step title="Generate the app secret">
    Generate or rotate the app secret from the AGG admin dashboard, then store it in your backend
    secrets manager. Never expose it to the browser.
  </Step>
  <Step title="Sign the assertion on your backend">
    Build an HMAC over `{externalId}:{timestamp}` using the app secret. This proves the assertion
    came from your backend.
  </Step>
  <Step title="Send the assertion to the frontend">
    Return `{ externalId, timestamp, hmac }` from your own API after the user is authenticated in
    your app.
  </Step>
  <Step title="Link it in AGG">
    Call `client.linkExternalId(assertion)` or the `useExternalId()` hook. AGG returns the updated
    `UserProfile` with `externalId` populated.
  </Step>
</Steps>

## Webhook-assisted linking

If your backend needs to connect new AGG users to existing partner users, subscribe to
`accounts.created`. The event includes the AGG `userId` and, when available from magic-link or
OAuth sign-in, `email`.

Use that email to resolve your internal user record, then complete the normal signed assertion flow
for that authenticated user. After `client.linkExternalId(assertion)` succeeds, AGG emits
`accounts.linked` with the AGG `userId` and your `externalId`.

```typescript
import { parseWebhookEvent } from "@agg-build/sdk/server";

const event = parseWebhookEvent(rawBody, req.headers, process.env.AGG_WEBHOOK_SECRET!);

if (event.type === "accounts.created" && event.data.email) {
  const partnerUser = await users.findByEmail(event.data.email);
  if (partnerUser) {
    await pendingLinks.store({
      aggUserId: event.data.userId,
      externalId: partnerUser.id,
    });
  }
}
```

<Note>
  `accounts.created.data.email` is `null` when AGG does not have an email for the principal, such
  as wallet-only sign-in or OAuth providers that do not return email.
</Note>

## Examples

### Backend signing

<Tabs>
  <Tab title="Node.js">
    <div key="node-js" className="[&>p]:mb-4 [&>p:last-child]:mb-0">
      ```typescript
      import { signExternalId } from "@agg-build/sdk/server";

      // Example: partner user record from your own database/session
      const externalId = currentPartnerUser.id;

      const assertion = signExternalId(process.env.AGG_APP_SECRET!, externalId);

      // Return this from your backend to your frontend
      return assertion;
      ```
    </div>
  </Tab>

  <Tab title="Python">
    <div key="python" className="[&>p]:mb-4 [&>p:last-child]:mb-0">
      ```python
      import hashlib
      import hmac
      import time

      external_id = current_partner_user["id"]
      timestamp = int(time.time() * 1000)
      payload = f"{external_id}:{timestamp}".encode()

      digest = hmac.new(
        bytes.fromhex(AGG_APP_SECRET),
        payload,
        hashlib.sha256,
      ).hexdigest()

      assertion = {
        "externalId": external_id,
        "timestamp": timestamp,
        "hmac": digest,
      }
      ```
    </div>
  </Tab>
</Tabs>

### Frontend linking

<Tabs>
  <Tab title="SDK">
    <div key="sdk" className="[&>p]:mb-4 [&>p:last-child]:mb-0">
      ```typescript
      import { createAggClient } from "@agg-build/sdk";

      const client = createAggClient({
        baseUrl: "https://api.agg.market",
        appId: "your-app-id",
      });

      // assertion comes from your backend
      const profile = await client.linkExternalId(assertion);

      console.log(profile.externalId);
      // "partner-user-123"
      ```
    </div>
  </Tab>

  <Tab title="Hooks">
    <div key="hooks" className="[&>p]:mb-4 [&>p:last-child]:mb-0">
      ```tsx
      import { useEffect, useRef } from "react";
      import { useExternalId } from "@agg-build/hooks";

      function LinkPartnerAccount({
        assertion,
      }: {
        assertion: { externalId: string; timestamp: number; hmac: string } | null;
      }) {
        const { linkExternalId } = useExternalId();
        const lastLinkedAssertion = useRef<string | null>(null);

        useEffect(() => {
          if (!assertion) return;

          const key = `${assertion.externalId}:${assertion.timestamp}`;
          if (lastLinkedAssertion.current === key) return;

          lastLinkedAssertion.current = key;

          void linkExternalId(assertion).catch(() => {
            lastLinkedAssertion.current = null;
          });
        }, [assertion, linkExternalId]);

        return null;
      }
      ```
    </div>
  </Tab>
</Tabs>

Once linked, `externalId` is returned on the updated `UserProfile` and on future
`client.getCurrentUser()` calls.
