Search documentation

Search documentation

Identity linking

When someone messages your agent on a channel like Slack or Discord, Pillar knows their channel user ID but not who they are in your app. Identity linking maps a channel user (e.g., Discord user 284102930) to your app's user (e.g., user_7842). Once linked, every tool call includes the external_user_id so your backend can look up the right account.

Without linking, tools see an anonymous caller. With linking, tools see external_user_id: "user_7842" and can do things like "look up this user's subscription" or "create a plan for this user."

The mechanism is a code exchange. Pillar generates a one-time code tied to a channel user. Your backend confirms the code with the user's ID in your system.

Two methods

MethodHow it works
OAuth (preferred)Users authenticate through your existing login flow. Your backend confirms the link with a code exchange.
APIYour backend creates and confirms link codes directly. Build any flow you want: slash commands, onboarding emails, bulk imports, custom UI.

For web-based agents using the browser SDK, use identify() instead — it links the user directly without a code exchange. See Adding User Context.

Method 1: OAuth (preferred)

If your app supports OAuth or has a web login page, users authenticate through your existing flow. After they log in, your backend calls confirmLink with the link code and the user's ID.

For web-based agents, use identify() instead:

typescript
pillar.identify('user_7842', {
email: 'sarah@acme.com',
name: 'Sarah Chen',
});

See Adding User Context for the full identify() API.

Method 2: API

Create and confirm link codes using the REST API or server SDKs.

bash
curl -X POST https://help-api.trypillar.com/api/public/identity/link-request/ \
-H "Authorization: Bearer plr_your_secret" \
-H "Content-Type: application/json" \
-d '{
"channel": "discord",
"channel_user_id": "284102930",
"channel_display_name": "Sarah",
"channel_email": "sarah@acme.com"
}'

Response:

json
{
"code": "aBcDeFgHiJkLmNoPqRsT",
"link_url": "https://yourapp.com/connect?code=aBcDeFgHiJkLmNoPqRsT",
"expires_at": "2025-01-15T10:05:00Z"
}
bash
curl -X POST https://help-api.trypillar.com/api/public/identity/link-confirm/ \
-H "Authorization: Bearer plr_your_secret" \
-H "Content-Type: application/json" \
-d '{
"code": "aBcDeFgHiJkLmNoPqRsT",
"external_user_id": "user_7842"
}'

Response:

json
{
"success": true,
"mapping": {
"channel": "discord",
"channel_user_id": "284102930",
"external_user_id": "user_7842",
"linked_at": "2025-01-15T10:02:30Z"
}
}

Check if a user is linked

bash
curl "https://help-api.trypillar.com/api/public/identity/resolve/?channel=discord&channel_user_id=284102930" \
-H "Authorization: Bearer plr_your_secret"

Response:

json
{
"is_linked": true,
"external_user_id": "user_7842",
"email": "sarah@acme.com",
"linked_at": "2025-01-15T10:02:30Z"
}

Confirming with the server SDK

TypeScript:

typescript
import { Pillar } from '@pillar-ai/server';
const pillar = new Pillar({
secret: process.env.PILLAR_SECRET!,
baseUrl: process.env.PILLAR_BASE_URL,
});
app.get('/connect', async (req, res) => {
const code = req.query.code as string;
const user = req.user;
await pillar.confirmLink({
code,
externalUserId: user.id,
});
res.send('Connected!');
});

Python:

python
from pillar import Pillar
pillar = Pillar(secret="plr_...", base_url="https://help-api.trypillar.com")
def connect_view(request):
code = request.GET["code"]
user = request.user
pillar.confirm_link(code=code, external_user_id=str(user.id))
return HttpResponse("Connected!")

Built-in /connect command

Slack and Discord agents include a /connect slash command that triggers this flow automatically. When a user types /pillar connect, Pillar generates the code and sends them a private link to your app. You handle the callback and confirm the code.

If you use the built-in /connect command, set the Identity Link URL in your product settings with a {code} placeholder:

https://yourapp.com/connect?code={code}

Pillar replaces {code} with the actual code when generating the link. This is only needed for the built-in command. If you build a custom flow, you control the URL yourself.

Security

  • Codes are cryptographically random (128-bit entropy) and single-use
  • Codes expire after 5 minutes
  • Generating a new code invalidates any existing unused code for that user
  • The link-confirm and link-request endpoints are rate-limited to 10 requests per minute
  • Codes are scoped to the product that generated them — a code from Product A cannot be confirmed with Product B's secret
  • All endpoints require authentication via Authorization: Bearer plr_... or x-customer-id header

Next steps

  • Adding User Context — Use identify() for web-based agents
  • Defining Tools — Tools receive caller.external_user_id when the user is linked
  • Slack — Channel setup and identity for Slack
  • Discord — Channel setup and identity for Discord