How Link works
Scope note: this page describes external Org API usage from proto/org.proto.
Org API Methods
proto/org.proto includes these public Org RPCs:
StreamDeviceUpdates(StreamDeviceUpdatesRequest): streams incremental device updates and deletions.SetConfiguration(Configurations): applies config updates by device ID.SendLockMode(SetDeviceModeRequest): applies lock mode operations (for example install mode transitions).SendDeviceKeys(SendDevicesKeysRequest): provisions keys for one or more devices.HardResetDevices(HardResetDevicesRequest): executes hard reset for one or more devices.SetOrgKeys(OrganizationKeys): deprecated in favor ofSendDeviceKeys; errors for Link client v2+ and will be removed in a future release.Assign(AssignmentsRequest): applies desired credential/set state and returnsAssignmentsResponse.
Link allows you to control and monitor your Last Lock devices. This tutorial will walk through: finding your device, listening to audits on your device, assigning credentials to your device, streaming audits.
All messages Link sends, both assigning credentials and access audits, are sent encrypted through a network of bridge devices. Bridges can never decrypt messages. Be sure you have a bridge nearby to ferry messages between devices and Link.
If you want to make your app a bridge, all you need to do is import the Proxy Kotlin Multiplatform library.
Below is a diagram of Link. Integrations only need to talk directly to their org server. Forwarding of information to and from your device is done by bridges over the Plane server. You do not need to directly integrate with Plane to forward information. You just have to import the Proxy Library.
Talking to your Org server
First, let's create an Org client for your Org server.
This tutorial assumes your Link key is set in your environment as link_key.
Ask your Last Lock contact for this key.
import grpc
import org_pb2
import org_pb2_grpc
import os
host = 'yourorg.seamless.lastlock.com'
port = "443"
channel_with_key = grpc.secure_channel(
host+":"+port,
grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(),
grpc.access_token_call_credentials(os.getenv("link_key"))
)
)
org = org_pb2_grpc.OrgStub(channel_with_key)
Checking that we can find your device
We can do this by asking your Org server for that device's summary. In the example below, we use 14, but you should be able to find your device's ID on its packaging.
response = org.ListDeviceSummaries(org_pb2.ListDevicesRequest(
device_ids = [14]
))
print(response)
You should get a response like this:
device_summaries {
device_id: 14
time_created: 1745174255
}
You may notice that we passed device_ids above, not device_id.
This allows you to get many devices in one request.
Additionally, you can filter by device creation time.
We find this useful within access control systems to get all devices that were created
after a certain time to easily sync up with new devices.
response = org.ListDeviceSummaries(org_pb2.ListDevicesRequest(
created_at_or_after = 1
))
print(response)
Adding a credential to your device
Have a supported credential in Link ready— in this example we'll use a Prox card. Try scanning it with your device. You should see it flash red: no access. We can verify this with.
Note: we have this input here. We'll see how to remove it shortly, but you may have to loop a bit here waiting for the audit to come in.
input("I've waited a couple of seconds for an audit")
auditResponse = org.ListAudits(org_pb2.ListAuditsRequest(
device_ids = [14],
))
print(auditResponse)
We will see your credential come up like so. Hopefully your credential looks different!
Let's break this down into parts.
You'll see your device ID, an audit message, and a timestamp 'when server'.
When server is when this audit reached the server via a bridge.
It is a UTC UNIX timestamp.
Inside the audit, you'll see an audit type, a credential, and a timestamp 'when device'.
Here we see UnauthorizedUser as an audit type: makes sense.
You'll see a credential with a credential type and the data associated with your credential.
Here the PROX credential is the raw data on the prox fob in PROX_GENERIC format—a 6-byte
Little Endian encoding that includes both a preamble header and the card data.
For details on how this encoding works, see Prox Credential Formats.
For other credentials, this may be a public key or even an ID.
Not all audits will have credentials:
some locks, such as a padlock, have no user associated with locking the device,
so the credential field will be missing in this case.
Link can add new AuditType values over time. If your client receives an audit type it does not support,
drop/ignore that audit type value instead of failing the stream or request.
Link can also add new ReceivedAudit payload variants over time.
If your client encounters a payload variant it does not support, ignore that variant instead of failing the stream.
Finally you'll see when device:
this is when the audit occurred according to the device's clock.
The device's clock is not guaranteed to be monotonic:
secure time syncs may move the device's clock back.
audits {
device_id: 14
audit {
audit_type: UnauthorizedUser
credential {
credential_type: PROX_GENERIC
data: "dd64f7062000"
}
when_device: 1745179900
}
when_server: 1745179901
}
The data field contains the 6-byte credential in hex format. For PROX_GENERIC credentials,
this includes a preamble header followed by the card data, all in Little Endian byte order.
See Prox Credential Formats for encoding details and examples.
Let's now set the credentials on the device.
We'll grab the credential from the previous audit and assign your device to just allow access to that credential. This is a set: your Org server will automatically determine the required messages to send to add and remove credentials from your lock. If you do not pass a device ID in the assignments map below, it is unaffected. While a little chatty, we have found that a set is a reliable way to get correct credentials onto the device in an efficient way.
onlyAudit = auditResponse.audits[-1]
credential = onlyAudit.audit.credential
assignment = org_pb2.AssignmentsRequest(
assignments = {
14: org_pb2.Credentials(
credentials = [credential]
)
}
)
org.Assign(assignment)
In a couple of seconds (it's pretty fast), you should see your assignment propagated to the device. Scan the same credential again and you should see green.
You can now filter for new audits.
input("I've waited a couple of seconds for an audit")
auditRequest = org_pb2.ListAuditsRequest(
device_ids = [14],
server_time_after = onlyAudit.when_server
)
auditResponse = org.ListAudits(auditRequest)
print("unlock", auditResponse)
Streaming audits
How can we get seamless performance? Stream! So far we have polled devices for a list of devices. We can also stream audits, getting alerted each time an audit comes through from your device. We'll reuse the list request.
Coverage behavior note:
- If
server_time_afteris set onStreamAudits, Link widens the bootstrap fetch by 2 hours. - Effective lower bound is
max(0, server_time_after - 7200). - This only affects the initial catch-up fetch when the stream starts, and improves missed-audit coverage near stream startup.
for audit in org.StreamAudits(auditRequest):
print("audit", audit, flush=True)
Device writable audit
Link emits a writable/readiness audit when a device becomes ready for normal writes. Hard reset clears the stored writable state, so the audit is emitted again when the device returns from reset.
The writable audit is a normal persisted audit, so it is returned by both Org.ListAudits and
Org.StreamAudits. It is encoded as a ReceivedAudit.audit payload with audit.writable set to
true and audit.audit_type set to DeviceLifecycle. Consumers should identify the
writable/readiness audit by audit.writable set to true; DeviceLifecycle is shared by device
lifecycle/readiness events and is not a replacement for checking the payload fields.
device_id: 14
when_server: 1779815096
audit_id: "3c49be1f-6d7c-42ae-940a-84c668eadef2"
audit: {
audit_type: DeviceLifecycle
writable: true
}
For Aliro-capable devices, Link can also emit a separate reader public key audit when the key is captured:
device_id: 14
when_server: 1779815096
audit_id: "3c49be1f-6d7c-42ae-940a-84c668eadef2"
audit: {
audit_type: AliroReaderPublicKeyCaptured
aliro_reader_public_key: "\x04..."
}
When audit.aliro_reader_public_key is present, it is the raw 65-byte uncompressed P-256 point
(0x04 || X || Y). Treat DeviceSummary.aliro_reader_public_key as the source of truth for the
current reader key; use AliroReaderPublicKeyCaptured as a signal to re-read the device summary if
the audit payload does not include the key bytes.
This is distinct from the writable/readiness audit, so a device can produce both audit records. Do
not key either behavior off an audit_type alone.
Wallet Credentials
Wallet credentials can be assigned through Assign like any other credential once they are issued by your system.