Guides/How to issue hotel key cards with an API

Implementation guide

How to issue hotel key cards with an API

Seam lets your software issue hotel key cards: define the guest's access for their stay, write it onto plastic with the encoder at the desk, and the card opens the right doors until checkout, then stops on its own.

On this page
Quickstart

Issue a key card in 60 seconds

Create the grant, wait until the card is ready to encode, select your desk's encoder, then encode and confirm issuance. Set SEAM_API_KEY, SEAM_ACS_ENTRANCE_ID, SEAM_ACS_SYSTEM_ID, and SEAM_ACS_ENCODER_ID. The CLI and cURL examples also require jq. The example stay starts now and lasts two days; use your reservation's dates in production.

1// npm install seam
2// Set SEAM_API_KEY and SEAM_ACS_ENTRANCE_ID for a supported entrance.
3// Set SEAM_ACS_SYSTEM_ID and SEAM_ACS_ENCODER_ID for your front desk.
4import { Seam } from "seam"
5
6const seam = new Seam()
7const entranceId = process.env.SEAM_ACS_ENTRANCE_ID
8if (!entranceId) throw new Error("Set SEAM_ACS_ENTRANCE_ID")
9const startsAt = new Date()
10const endsAt = new Date(startsAt.getTime() + 2 * 24 * 60 * 60 * 1000)
11
12const accessGrant = await seam.accessGrants.create({
13  user_identity: { full_name: "Jane Doe", email_address: "jane@example.com" },
14  acs_entrance_ids: [entranceId],
15  requested_access_methods: [{ mode: "card" }],
16  starts_at: startsAt.toISOString(),
17  ends_at: endsAt.toISOString(),
18})
19
20// Poll at two-second intervals, at most 30 times.
21let cardMethod
22for (let attempt = 0; attempt < 30; attempt++) {
23  const methods = await seam.accessMethods.list({
24    access_grant_id: accessGrant.access_grant_id,
25  })
26  cardMethod = methods.find((method) => method.mode === "card")
27  if (cardMethod?.is_ready_for_encoding) break
28  await new Promise((resolve) => setTimeout(resolve, 2000))
29}
30if (!cardMethod?.is_ready_for_encoding) {
31  throw new Error("Access method is not ready for encoding yet. Check its errors and retry later.")
32}
33
34// Choose the encoder where the blank card is placed.
35const encoderId = process.env.SEAM_ACS_ENCODER_ID
36if (!encoderId) throw new Error("Set SEAM_ACS_ENCODER_ID for this desk")
37const encoders = await seam.acs.encoders.list({
38  acs_system_id: process.env.SEAM_ACS_SYSTEM_ID,
39})
40const frontDesk = encoders.find((encoder) => encoder.acs_encoder_id === encoderId)
41if (!frontDesk) throw new Error("Configured encoder was not found on this system")
42
43// Place the blank card on the selected encoder before running this call.
44await seam.accessMethods.encode({
45  access_method_id: cardMethod.access_method_id,
46  acs_encoder_id: frontDesk.acs_encoder_id,
47})
48
49for (let attempt = 0; attempt < 30; attempt++) {
50  cardMethod = await seam.accessMethods.get({ access_method_id: cardMethod.access_method_id })
51  if (cardMethod.is_issued) break
52  await new Promise((resolve) => setTimeout(resolve, 2000))
53}
54if (!cardMethod.is_issued) throw new Error("Card is not issued yet; check errors before handing it over")
Fastest path

Build it with your AI agent

Install the Seam plugin and your coding agent picks up three integration skills: Reservation Automations, Access Grants and Access Codes. It then writes the calls the way we would. Paste the prompt below and let it build.

1. Install the plugin

/plugin marketplace add seamapi/seam-plugin
/plugin install seam@seamapi

The Claude Code install also adds our documentation MCP server, so the agent can look up endpoints and device capabilities while it works. Cursor, Codex and other agents get the skills through npx skills add and can add the MCP server there separately.

2. Paste this prompt

Prompt
Using the Seam Access Grants skill, build key card issuance for my hotel app. In a sandbox workspace: create a user identity for a guest, create an access grant on the room and lobby entrances with requested_access_methods [{ mode: "card" }] for a two-night stay, wait for is_ready_for_encoding, list the encoders on the access system with /acs/encoders/list, then encode the card access method with /access_methods/encode and confirm is_issued flips to true. Finish with the express-checkout story: what happens to the card at ends_at, and how I re-encode the same plastic for the next guest.
Use cases

What you can build

  • Front desk software that cuts keys

    Check-in creates the grant and encodes the card in one flow, from your own UI. The desk agent taps one button; nobody re-types room numbers into the access system's console.

  • Kiosk and express check-in

    A kiosk with an encoder issues keys with no staff at all: the reservation lookup creates the grant, the kiosk encodes, the guest walks to the elevator. Late arrivals stop depending on the night shift.

  • Cards that expire with the stay

    The credential dies at ends_at whether or not the card comes back, and an extended stay is one update call, not a trip to the desk for new plastic. Lost cards are a deletion, not a re-key.

Walkthrough

How it works

  1. Create an Access Grant with a card access method

    Create the guest's grant for the entrances and reservation window. Use user_identity_id instead of the inline user_identity for an existing guest. Poll until the card access method has is_ready_for_encoding true; is_issued remains false until encoding completes. These steps reuse the client, imports, dates, and selected entrance from the quickstart.

    1const accessGrant = await seam.accessGrants.create({
    2  user_identity: { full_name: "Jane Doe", email_address: "jane@example.com" },
    3  acs_entrance_ids: [entranceId],
    4  requested_access_methods: [{ mode: "card" }],
    5  starts_at: startsAt.toISOString(),
    6  ends_at: endsAt.toISOString(),
    7})
    8
    9// Poll at two-second intervals, at most 30 times.
    10let cardMethod
    11for (let attempt = 0; attempt < 30; attempt++) {
    12  const methods = await seam.accessMethods.list({
    13    access_grant_id: accessGrant.access_grant_id,
    14  })
    15  cardMethod = methods.find((method) => method.mode === "card")
    16  if (cardMethod?.is_ready_for_encoding) break
    17  await new Promise((resolve) => setTimeout(resolve, 2000))
    18}
    19if (!cardMethod?.is_ready_for_encoding) {
    20  throw new Error("Access method is not ready for encoding yet. Check its errors and retry later.")
    21}
  2. Find the encoder at the desk

    List the encoders on your access system and select the one at this desk. The example matches SEAM_ACS_ENCODER_ID so it writes to the intended hardware. This guide covers encodable cards; systems with pre-registered cards use the card-assignment flow instead.

    1// Choose the encoder where the blank card is placed.
    2const encoderId = process.env.SEAM_ACS_ENCODER_ID
    3if (!encoderId) throw new Error("Set SEAM_ACS_ENCODER_ID for this desk")
    4const encoders = await seam.acs.encoders.list({
    5  acs_system_id: process.env.SEAM_ACS_SYSTEM_ID,
    6})
    7const frontDesk = encoders.find((encoder) => encoder.acs_encoder_id === encoderId)
    8if (!frontDesk) throw new Error("Configured encoder was not found on this system")
  3. Encode the card

    Place a blank card on the encoder and call /access_methods/encode with the access method and the encoder. Confirm the result before handing plastic to a guest: is_issued flips to true once the write lands, and the common failures are a missing card on the encoder or a card type the system does not accept.

    1// Place the blank card on the selected encoder before running this call.
    2await seam.accessMethods.encode({
    3  access_method_id: cardMethod.access_method_id,
    4  acs_encoder_id: frontDesk.acs_encoder_id,
    5})
    6
    7for (let attempt = 0; attempt < 30; attempt++) {
    8  cardMethod = await seam.accessMethods.get({ access_method_id: cardMethod.access_method_id })
    9  if (cardMethod.is_issued) break
    10  await new Promise((resolve) => setTimeout(resolve, 2000))
    11}
    12if (!cardMethod.is_issued) throw new Error("Card is not issued yet; check errors before handing it over")
  4. Let checkout happen on its own

    No return step. The credential expires at ends_at, so an unreturned card is dead plastic, and revoking early is deleting the grant. Reuse returned cards by encoding a fresh access method onto them; one access method writes to one card, so each card gets its own.

FAQ

Frequently asked questions

Do I need a card encoder?

For systems that write credentials onto blank cards, yes: Seam drives the encoder hardware the property already has. Other systems assign the credential to a card that is already registered, and no encoder is involved. The capability page lists which systems work which way.

Can one card open several doors?

Yes. Put every entrance on the Access Grant, or pass a space that covers them, and the card is encoded for all of them at once. Adding a door later updates the credential.

What happens to a card at checkout?

The credential expires with the grant, so the card stops opening doors at ends_at whether or not it comes back to the desk. Revoking early is a deletion, not a trip to the lock.

Can I reuse cards between guests?

Yes. Re-encode the card with the next guest's access method. What you cannot do is write one access method onto two cards: a second card for the same guest gets its own access method on the same grant.

How do I check what is on a card?

Scan it on the encoder. The scan returns the card's encoded parameters, which is how a desk agent answers "why does this card not work" without guessing: wrong stay, expired credential and blank card all read differently.

Which access systems does this work with?

Card-based hotel and multifamily systems including ASSA ABLOY Visionline and Salto Space, with the full list on the plastic-card capability page. Support is per system, so check the system behind your doors, not the lock brand on them.

Pricing

Ready to Scale With You

Seam lets developers and businesses connect apps to real-world IoT devices. The free trial is built for small teams to build and test, while enterprise pricing scales with the biggest device fleets out there.
Best Enterprise Solution

Custom Subscription for Your Business

Access tailored solutions, paying a fixed monthly rate that scales as you grow.
Enhanced Security
Beta Features Access
Dedicated Support Channel
Volume Discounts
No Usage Limits
Trusted by Over 1,000 Companies
Start Now

Try It for Free

Start with a free trial and explore the full API and related features.

Have questions? Talk to our team.

Chat with us to learn how Seam can power your access automation.
—“Discovering Seam was one of these Aha moments in the business. I couldn’t believe what I was seeing. I mean, it was exactly what we were looking for: a simple API that we could integrate and have access to all these devices in a matter of seconds.”
François Gouelo
CEO, Enso Connect
Company
How many devices you need to connect?
Name
Email
How did you find us?
Comment
We usually answer within 24 hours.
One API for All Devices
Global Infrastructure for Controlling the Physical World
Seam makes controlling IoT devices seamless and programmable. Our teams operate globally,
enabling businesses of all sizes.
Smart Locks
Access Systems
Knowledge
Lock Capabilities
Thermostats
Sensors
© 2026 Seam Labs, Inc.