Link an account
Linking creates a Pix enrollment (a wallet) that binds a payer's account
at their bank to a passkey on their device. Every payment flow requires an
AUTHORISED enrollment first, so this is the flow you integrate before any
payment.
The enrollment is authorized without redirection: the payer authenticates once at their bank, then registers a FIDO2 credential on their device. From then on, payments are approved with that passkey.
Method sequence
participants.listMostUsed(subTenantId)— list banks enabled for Pix, most-used first. Filter to Pix-capable servers with thepixEnabledServershelper and let the payer pick one. UselistRegistered()as a fallback.enrollments.listByUserJsr(cpf, { deviceId })— check whether the payer already has anAUTHORISEDenrollment (skip linking if so).enrollments.createJsr(req)— open the enrollment. Returns{ id, redirectUrl }. OpenredirectUrlon the device.- [device] the payer authenticates at the bank; the bank redirects back
with
state,code, andidToken, which the device delivers to your backend. enrollments.getDeviceOptionsJsr(req)— exchange those callback params for FIDO2 registration options.- [device] the device calls
navigator.credentials.create()with those options and produces an attestation. enrollments.registerDeviceJsr(enrollmentId, attestation)— bind the credential. The enrollment becomesAUTHORISED.
Sequence diagram
Create the enrollment
Steps 1–3: list banks, check for an existing enrollment, and create a new
one. createJsr requires riskSignals collected from the device (device id,
timezone, language, screen size, account tenure), and the X-Client-IP
header passed as clientIp.
import { pixEnabledServers } from '@linainfratech/embedded-payments';
const cpf = '39033521016';
const clientIp = '203.0.113.10'; // payer device IP -> X-Client-IP
const riskSignals = {
deviceId: 'device-abc',
userTimeZoneOffset: '-03:00',
language: 'pt-BR',
screenDimensions: { width: 390, height: 844 },
accountTenure: '2024-01-01',
};
// 1) Banks enabled for Pix (most-used first; fallback to listRegistered)
const participants = subTenantId
? await client.participants.listMostUsed(subTenantId)
: await client.participants.listRegistered();
const bank = pixEnabledServers(participants)[0];
// 2) Skip linking if an AUTHORISED enrollment already exists
const existing = await client.enrollments.listByUserJsr(cpf, { deviceId: riskSignals.deviceId });
// 3) Create the enrollment
const created = await client.enrollments.createJsr({
organisationId: bank.OrganisationId,
authorisationServerId: bank.AuthorisationServerId,
enrollment: { document: cpf, deviceName: 'My Backend' },
riskSignals,
redirectUri: 'https://app.example.com/authorization-success',
}, { clientIp });
// Open created.redirectUrl on the device.String cpf = System.getenv("CPF");
String ip = "203.0.113.10";
RiskSignals risk = new RiskSignals()
.deviceId("device-abc").userTimeZoneOffset("-03:00").language("pt-BR")
.screenDimensions(new ScreenDimensions(390, 844)).accountTenure("2024-01-01");
// 1) Banks enabled for Pix (most-used first; fallback to listRegistered)
List<Participant> participants = subTenantId != null
? client.participants.listMostUsed(subTenantId)
: client.participants.listRegistered();
PixServer bank = Helpers.pixEnabledServers(participants).get(0);
// 2) Skip linking if an AUTHORISED enrollment already exists
UserEnrollments existing = client.enrollments.listByUserJsr(cpf, ListEnrollmentsQuery.deviceId("device-abc"));
// 3) Create the enrollment
CreateEnrollmentResult created = client.enrollments.createJsr(
new CreateEnrollmentRequest()
.organisationId(bank.organisationId())
.authorisationServerId(bank.authorisationServerId())
.enrollment(new EnrollmentInfo().document(cpf).deviceName("My Backend"))
.riskSignals(risk)
.redirectUri("https://app.example.com/authorization-success"),
CallOptions.clientIp(ip));
// Open created.redirectUrl() on the device.let cpf = ProcessInfo.processInfo.environment["CPF"] ?? ""
let ip = "203.0.113.10"
let risk = RiskSignals(
deviceId: "device-abc", userTimeZoneOffset: "-03:00", language: "pt-BR",
screenDimensions: ScreenDimensions(width: 390, height: 844), accountTenure: "2024-01-01")
// 1) Banks enabled for Pix (most-used first; fallback to listRegistered)
let participants: [Participant]
if let subTenantId { participants = try await client.participants.listMostUsed(subTenantId) }
else { participants = try await client.participants.listRegistered() }
guard let bank = Helpers.pixEnabledServers(participants).first else { return }
// 2) Skip linking if an AUTHORISED enrollment already exists
let existing = try await client.enrollments.listByUserJsr(cpf, query: ListEnrollmentsQuery(deviceId: "device-abc"))
// 3) Create the enrollment
let created = try await client.enrollments.createJsr(
CreateEnrollmentRequest(
organisationId: bank.organisationId,
authorisationServerId: bank.authorisationServerId,
enrollment: EnrollmentInfo(document: cpf, deviceName: "My Backend"),
riskSignals: risk,
redirectUri: "https://app.example.com/authorization-success"),
options: .clientIp(ip))
// Open created.redirectUrl on the device.var cpf = Environment.GetEnvironmentVariable("CPF") ?? "";
const string ip = "203.0.113.10";
var risk = new RiskSignals
{
DeviceId = "device-abc", UserTimeZoneOffset = "-03:00", Language = "pt-BR",
ScreenDimensions = new ScreenDimensions { Width = 390, Height = 844 },
AccountTenure = "2024-01-01",
};
// 1) Banks enabled for Pix (most-used first; fallback to ListRegisteredAsync)
var participants = subTenantId is not null
? await client.Participants.ListMostUsedAsync(subTenantId)
: await client.Participants.ListRegisteredAsync();
var bank = Helpers.PixEnabledServers(participants)[0];
// 2) Skip linking if an AUTHORISED enrollment already exists
var existing = await client.Enrollments.ListByUserJsrAsync(cpf, new ListEnrollmentsQuery { DeviceId = "device-abc" });
// 3) Create the enrollment
var created = await client.Enrollments.CreateJsrAsync(new CreateEnrollmentRequest
{
OrganisationId = bank.OrganisationId,
AuthorisationServerId = bank.AuthorisationServerId,
Enrollment = new EnrollmentInfo { Document = cpf, DeviceName = "My Backend" },
RiskSignals = risk,
RedirectUri = "https://app.example.com/authorization-success",
}, CallOptions.WithClientIp(ip));
// Open created.RedirectUrl on the device.Register the device
After the bank redirects back with state, code, and idToken, exchange
them for FIDO2 options (step 5), let the device build the attestation (step
6), then bind it (step 7). The example below is Node; the other SDKs expose
the same two methods — getDeviceOptionsJsr and registerDeviceJsr — with
idiomatic names.
// 5) Exchange callback params for FIDO2 registration options
const options = await client.enrollments.getDeviceOptionsJsr({
state, // from the bank redirect — must match what you persisted in step 3
code,
idToken,
tenantId: 'lina',
platform: 'ANDROID', // ANDROID | IOS | WEB | BROWSER | CROSS_PLATFORM
}, { clientIp });
// 6) [device] navigator.credentials.create(options) -> attestation
// 7) Register the device with the attestation returned by the device
const enrollment = await client.enrollments.registerDeviceJsr(options.enrollmentId, {
id: attestation.id,
rawId: attestation.rawId,
response: {
clientDataJSON: attestation.response.clientDataJSON,
attestationObject: attestation.response.attestationObject,
},
}, { clientIp });
console.log(enrollment.status); // AUTHORISEDregisterDeviceJsr returns the enrollment with one of six statuses:
| Status | Meaning | What to do |
|---|---|---|
AWAITING_RISK_SIGNALS | The enrollment was created but risk signals haven't been accepted yet. | Retry with valid riskSignals, or wait — this is transient. |
AWAITING_ACCOUNT_HOLDER_VALIDATION | The bank hasn't confirmed the payer's authorization yet. | Poll or wait for the redirect callback; don't call registerDeviceJsr early. |
AWAITING_ENROLLMENT | Authorized at the bank, but device registration (steps 5–7) isn't complete. | Finish the device registration steps. |
AUTHORISED | Ready to use. | Use this enrollment's enrollmentId to pay. |
REJECTED | The bank or the payer rejected the enrollment. | Send the payer through account linking again from step 1. |
REVOKED | Revoked via revokeJsr (or by the payer at their bank). | Same as REJECTED — link again. |
Revoke an enrollment
Revoking is a single call — a logical revocation with no body. Pass the same
clientIp used elsewhere in JSR flows.
await client.enrollments.revokeJsr(enrollmentId, { clientIp });client.enrollments.revokeJsr(enrollmentId, CallOptions.clientIp(ip));try await client.enrollments.revokeJsr(enrollmentId, options: .clientIp(ip))await client.Enrollments.RevokeJsrAsync(enrollmentId, CallOptions.WithClientIp(ip));Next steps
- Pay a QR Code — pay a boleto or Pix QR Code with the linked account.
- Pix transfer by key — send a Pix transfer to a Pix key.
- Error handling — handle
DependencyErrorfrom the account holder and other failures.