Integration Guide

for 7punches Embedded web terminal

Last Updated: Aug 25, 2026

Overview

This guide is for POS/partner engineers embedding the 7shifts 7Punches time-clock terminal into their system. The primary, expected integration is a native mobile webview and this guide is written for that path. A web iframe is also supported for web/hybrid hosts but carries extra constraints; it is documented separately.

Post message payloads referenced throughout can be found in the 7Punches postMessage Protocol Reference (provided separately).

Key concepts: terminal login vs PIN entry

To start, let’s discuss two different “login” concepts for clarity:

  1. Terminal login (device session): this is a 7shifts user. The terminal must be signed in by a 7shifts user, this is typically a manager or admin, through an interactive user sign-in. That user's session is what authorizes the device to call the 7shifts API. It is done once; the terminal then persists the resulting user refresh token so subsequent launches resume silently.

  2. PIN entry (employee identification): per punch, not authentication. Once the device is signed in, individual employees enter a PIN (or direct sign in via external ID) to identify themselves and punch. The PIN does not authenticate to the API. It selects who is punching within the already-authenticated manager session.

Important: partner API OAuth client credentials cannot sign the terminal in. Partners often already have an OAuth client for calling 7shifts APIs, however client credential logins are not accepted or used by the 7shifts terminal.

Quick start (the happy path)

At a glance, a native webview integration is:

  1. Embed: load the hosted terminal URL in your webview and register the sevenPunches native bridge.
  2. Wait for 7punches:ready: the terminal emits this once its message listener is installed. Do not send anything before it.
  3. Send 7punches:init: with companyId and applicationName (plus the saved refreshToken if you have one).
  4. First sign-in: with no saved token, a manager/admin signs in interactively; the terminal returns a user refresh token to you via 7punches:session_token. Save it securely.
  5. Resume: on later launches, pass the saved token in init for silent resume. The token rotates, so always persist the newest one.

Employees then punch by PIN (or you can skip the PIN and target a single user. See Identity / operating mode). The rest of this guide details each step.

Reference Information

ItemNotes
Terminal URLshttps://7punches.7shifts.com
applicationNameAssigned integration identifier. Required at init; scopes user/role mapping to your integration.

1. Embed the terminal (native webview)

The terminal auto-detects and routes outbound messages to the first available native bridge, so you only need to register the one your platform uses. Inbound messages (init, etc.) are delivered to the terminal's window message listener.

  • iOS (WKWebView). Register a script message handler named sevenPunches: config.userContentController.add(handler, name: "sevenPunches")
  • Android (WebView). Register a JavaScript interface named sevenPunches: webView.addJavascriptInterface(bridge, "sevenPunches")
  • React Native. react-native-webview provides window.ReactNativeWebView automatically. No bridge registration needed.

The handler name is sevenPunches on both platforms. Register your native bridge under the exact name sevenPunches on iOS and Android. React Native needs no registration.

Receiving messages from the terminal

Outbound messages arrive on the bridge you registered. Note the payload encoding differs per platform: iOS WKWebView delivers a native object/dictionary, while Android and React Native deliver a JSON string you must parse.

iOS (Swift):

// Registration
let contentController = WKUserContentController()
contentController.add(self, name: "sevenPunches")
let config = WKWebViewConfiguration()
config.userContentController = contentController

// Handle inbound messages (body is a dictionary on iOS)
extension TerminalViewController: WKScriptMessageHandler {
    func userContentController(_ c: WKUserContentController,
                              didReceive message: WKScriptMessage) {
        guard message.name == "sevenPunches",
              let body = message.body as? [String: Any],
              let type = body["type"] as? String else { return }
        let payload = body["payload"] as? [String: Any]
        switch type {
        case "7punches:ready":
            sendInit()                                   // see section 3
        case "7punches:session_token":
            if let token = payload?["refreshToken"] as? String {
                Keychain.save(refreshToken: token)       // persist securely
            }
        case "7punches:session_expired":
            Keychain.clearRefreshToken()
        default:
            handleTerminalEvent(type: type, payload: payload)  // punch_success/_error/exit
        }
    }
}

Android (Kotlin):

class SevenPunchesBridge(
    private val onMessage: (type: String, payload: JSONObject?) -> Unit
) {
    // The terminal calls window.sevenPunches.postMessage(<json string>)
    @JavascriptInterface
    fun postMessage(json: String) {
        val msg = JSONObject(json)
        onMessage(msg.getString("type"), msg.optJSONObject("payload"))
    }
}

// Registration
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(
    SevenPunchesBridge { type, payload ->
        // @JavascriptInterface runs on a background thread marshal to main
        // before touching the WebView or UI.
        when (type) {
            "7punches:ready" -> runOnUiThread { sendInit() }          // see section 3
            "7punches:session_token" ->
                payload?.optString("refreshToken")?.let { secureStore.save(it) }
            "7punches:session_expired" -> secureStore.clear()
            else -> handleTerminalEvent(type, payload)
        }
    },
    "sevenPunches"
)

2. Initialization handshake

  1. Load the terminal URL in your webview.
  2. Wait for the terminal to emit 7punches:ready. Do not send init before this. The terminal installs its message listener before emitting ready, and messages sent earlier are missed.
  3. Send 7punches:init.

Minimal init:

{ "type": "7punches:init", "payload": {
    "companyId": 12345,          // required (number)
    "applicationName": "toast"   // required (non-empty string; your assigned name)
} }

Full init (all optional fields):

{ "type": "7punches:init", "payload": {
    "companyId": 12345,
    "applicationName": "yourappname",
    "refreshToken": "def502...",       // saved user token → silent resume (section 3)
    "theme": "dark",                    // "light" | "dark"
    "lang": "fr-CA",                    // BCP-47 tag for UI locale
} }

Unknown/invalid fields are ignored; companyId and a non-empty applicationName are mandatory or the init is dropped. See the 7Punches postMessage Protocol Reference for full payload details.

3. Authentication & session (terminal login)

Terminal login establishes the user (manager/admin) session described in Key concepts above. There is one flow with two phases: an interactive first sign-in, then silent resume from a saved token.

Phase 1: First sign-in (interactive user OAuth)

On first launch (no saved refresh token), the terminal shows a Sign-in screen, completing a login issues a user refresh token. A manager or admin signs in with their 7shifts credentials. In a native webview the entire round-trip happens inside the webview as a top-level navigation and the token is returned to your host over the native bridge.


Sending init from native code

Deliver init to the terminal as a window message event (its listener handles inbound messages). Build the JSON, include the saved refreshToken only when resuming (Phase 2), and inject it once you've received 7punches:ready.

Phase 2: Silent resume (host provides the saved token)

On every subsequent launch, pass the saved refresh token in init:

{ "type": "7punches:init", "payload": {
    "companyId": 12345,
    "applicationName": "appname",
    "refreshToken": "def502..."   // the refresh token saved from Phase 1
} }

The terminal silently exchanges it for a fresh access token and re-emits 7punches:session_token on every refresh.

  • Persist the refresh token from each session_token securely, and pass the latest one on the next launch. Tokens rotate upon use, so always store the newest.
  • If the saved token is rejected, the terminal falls back to Phase 1 (interactive sign-in).

Note this is a user token, not a client credentials token. The saved refresh token belongs to the manager/admin user who signed in.

Session expiry

When the terminal can't recover a session on its own it emits 7punches:session_expired. Handle this by clearing the stored token and returning the terminal to Phase 1 (a manager re-signs in). Refresh token lifetimes are 1 month by default.

4. Identity / operating mode

PIN pad (default, shared terminal)

With no externalUserId, the terminal is a shared, company-scoped device: employees enter their PIN to punch. Suited to a fixed back-of-house terminal.

External-user mode (single user, skips PIN)

Pass externalUserId at init and the terminal skips the PIN pad, resolves your ID to the employee (via the mapping we configure), and lands directly on that user's status screen. When the user finishes, the terminal emits 7punches:exit (instead of returning to a PIN pad). The host should close or navigate away from the webview. Re-target a different user without a full re-init using 7punches:set_external_user.

5. Receiving punch results and events

Outbound events (full shapes in the 7Punches postMessage Protocol Reference):

EventHandle by
7punches:readySend init
7punches:session_tokenPersist the refresh token
7punches:session_expiredRe-authenticate
7punches:punch_successRecord the punch (enriched for clock in/out; thin for breaks; offline:true when queued offline)
7punches:punch_errorSurface/log the failure (category + message)
7punches:exitClose the webview (external-user mode)

Did this page help you?