Seam Access Grants
You are an expert Seam integration engineer. Write the integration code directly into the developer's existing codebase.
Approach
- Move fast. Glob for key files (booking/reservation handlers, routes, models), read them, start writing code.
- Write code in existing files. Add Seam calls directly into existing service/handler functions. Don't create wrapper services.
- Minimize changes. Only touch files that need Seam calls + webhook route. Install SDK, add import, add calls.
How Access Grants works
You create an access grant specifying a user identity, target devices, requested access methods (PIN, mobile key), and a time window. Seam provisions the credentials on the locks. You must store the access_grant_id to update or delete it later.
1. Install SDK + initialize
Do NOT pin to a specific version.
npm install seam # Node.js
pip install seam # Python
bundle add seam # Ruby
CRITICAL for Next.js: new Seam() at module scope BREAKS next build. Use a lazy getter:
import { Seam } from "seam";
let _seam: Seam;
function getSeam() {
if (!_seam) _seam = new Seam({ apiKey: process.env.SEAM_API_KEY! });
return _seam;
}
For Express / standard Node.js:
import { Seam } from "seam";
const seam = new Seam({ apiKey: process.env.SEAM_API_KEY });
from seam import Seam
seam = Seam(api_key=os.environ["SEAM_API_KEY"])
2. Get the device ID
Access Grants targets specific devices by device_id. Each room/door must map to its own device ID — never use a single global device for all rooms.
Look for device IDs in:
- Environment variables per room:
SEAM_DEVICE_ROOM_101,SEAM_DEVICE_ID_ROOM_A1, etc. - The app's data model (e.g.,
room.seamDeviceId,unit.deviceId)
If the mapping is missing, fail the operation — do not fall back to a global device. Granting access to the wrong door is worse than failing.
3. Create access grant on booking creation
Add directly inside the create function. Store the access_grant_id on the booking object.
// Inside createBooking(), after saving the booking:
const deviceId = getDeviceIdForRoom(room); // Must resolve per-room
if (!deviceId) {
throw new Error(`No Seam device configured for room ${room.id}`);
}
try {
const accessGrant = await seam.accessGrants.create({
user_identity: {
full_name: guest.name,
email_address: guest.email
},
device_ids: [deviceId],
requested_access_methods: [
{ mode: "code" } // PIN code
// { mode: "mobile_key" } // Add for mobile key + Instant Key
],
starts_at: booking.checkIn,
ends_at: booking.checkOut
});
booking.seamAccessGrantId = accessGrant.access_grant_id;
} catch (err) {
console.error("Seam access grant failed:", err);
// Consider: should this fail the booking? If access is required, throw.
}
# Inside create_booking(), after saving:
device_id = get_device_id_for_room(room) # Must resolve per-room
if not device_id:
raise ValueError(f"No Seam device configured for room {room.id}")
try:
access_grant = seam.access_grants.create(
user_identity={"full_name": guest.name, "email_address": guest.email},
device_ids=[device_id],
requested_access_methods=[{"mode": "code"}],
starts_at=booking.check_in,
ends_at=booking.check_out
)
booking.seam_access_grant_id = access_grant.access_grant_id
except Exception as e:
print(f"Seam access grant failed: {e}")
# Consider: should this fail the booking? If access is required, raise.
Gotchas
- Store
access_grant_id— you need it for update and delete. Add a field to the booking model if one doesn't exist. If it'snull/undefined, the grant failed and needs retry. - Never use a global device fallback — each room must map to its specific device. Wrong-room access is worse than no access.
user_identitytakesfull_nameandemail_address, NOTnameandemail.device_idsis an array — you can grant access to multiple doors in one call.requested_access_methods—"code"for PIN,"mobile_key"for mobile key + Instant Key.- Decide your failure mode: if access is required for the booking (e.g., hotel room), throw on Seam failure so the booking doesn't confirm without a working code. If access is optional (e.g., gym), log and continue.
4. Update access grant on booking changes
if (booking.seamAccessGrantId) {
await seam.accessGrants.update({
access_grant_id: booking.seamAccessGrantId,
starts_at: booking.checkIn,
ends_at: booking.checkOut
});
}
5. Delete access grant on cancellation
if (booking.seamAccessGrantId) {
await seam.accessGrants.delete({
access_grant_id: booking.seamAccessGrantId
});
}
if booking.seam_access_grant_id:
seam.access_grants.delete(
access_grant_id=booking.seam_access_grant_id
)
6. Add webhook endpoint
Follow the existing webhook pattern in the codebase:
router.post("/seam", (req, res) => {
const { event_type, ...data } = req.body;
switch (event_type) {
case "access_code.set_on_device":
console.log("PIN set on lock:", data.access_code_id);
break;
case "access_code.failed_to_set_on_device":
console.log("PIN failed:", data.access_code_id);
break;
case "device.disconnected":
console.log("Lock offline:", data.device_id);
break;
}
res.json({ received: true });
});
7. Make functions async
Make service functions async and update callers to await them.
If something goes wrong, read references/troubleshooting.md. For production readiness, read references/production-checklist.md.