Account

Partner External ID Linking

Link an authenticated sufwan-local-test user to your app's internal user ID with an HMAC assertion

This flow is for partners who need a trusted mapping between an sufwan-local-test user.id and their own internal user ID for rewards, analytics, or reconciliation. The assertion must come from your backend, not from the browser.

The HMAC externalId is the durable partner user id. It is not the venue externalIdentifier and not the webhook partner id. See Ids.

How it works

Generate the app secret

Generate or rotate the app secret from the sufwan-local-test admin dashboard, then store it in your backend secrets manager. Never expose it to the browser. The secret is hex.

Sign the assertion on your backend

Sign HMAC-SHA256 over the string {externalId}:{timestamp}. timestamp is an integer in milliseconds. The key is that hex secret decoded from hex, not the hex text as UTF-8. Send the hex digest as hmac. A timestamp more than five minutes off the server clock is rejected.

Send the assertion to the frontend

Return { externalId, timestamp, hmac } from your own API after the user is authenticated in your app.

Link it in sufwan-local-test

Call client.linkExternalId(assertion) or the useExternalId() hook. sufwan-local-test returns the updated UserProfile with externalId populated.

Webhook-assisted linking

If your backend needs to connect new sufwan-local-test users to existing partner users, subscribe to accounts.created. The event includes the sufwan-local-test 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, sufwan-local-test emits accounts.linked with the sufwan-local-test userId and your externalId.

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,
    });
  }
}

accounts.created.data.email is null when sufwan-local-test does not have an email for the principal, such as wallet-only sign-in or OAuth providers that do not return email.

Examples

Backend signing

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;

Send the assertion with the user's access token. A 409 means that external id is already linked to another user in this app.

SDK
import { createAggClient } from "@agg-build/sdk";

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

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

Link partner external user ID (HMAC-verified).

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;
}

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

On this page