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

# Partner External ID Linking

> Link an authenticated AGG user to your app's internal user ID with an HMAC assertion

<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 theme={null}
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">
    ```typescript theme={null}
    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;
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    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,
    }
    ```
  </Tab>
</Tabs>

### Frontend linking

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    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"
    ```
  </Tab>

  <Tab title="Hooks">
    ```tsx theme={null}
    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;
    }
    ```
  </Tab>
</Tabs>

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