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
| Method | How it works |
|---|---|
| OAuth (preferred) | Users authenticate through your existing login flow. Your backend confirms the link with a code exchange. |
| API | Your 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:
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.
Generate a link code
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:
{"code": "aBcDeFgHiJkLmNoPqRsT","link_url": "https://yourapp.com/connect?code=aBcDeFgHiJkLmNoPqRsT","expires_at": "2025-01-15T10:05:00Z"}
Confirm a link code
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:
{"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
curl "https://help-api.trypillar.com/api/public/identity/resolve/?channel=discord&channel_user_id=284102930" \-H "Authorization: Bearer plr_your_secret"
Response:
{"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:
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:
from pillar import Pillarpillar = Pillar(secret="plr_...", base_url="https://help-api.trypillar.com")def connect_view(request):code = request.GET["code"]user = request.userpillar.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-confirmandlink-requestendpoints 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_...orx-customer-idheader
Next steps
- Adding User Context — Use
identify()for web-based agents - Defining Tools — Tools receive
caller.external_user_idwhen the user is linked - Slack — Channel setup and identity for Slack
- Discord — Channel setup and identity for Discord