Graine AI

Booking from your app with the SDK's sheet

Offer "Call me at…" with no screen of your own: when booking is available, open the SDK's booking sheet — the day, the time, the time zone, what it is about, Book call — in your agent's design, get the booked call back, and show every refusal in plain words. New in 0.8.0.

The SDK draws the whole booking screen, in your agent's design: day chips from today to the furthest day the agent allows, the time (each platform's own picker, only times the rules allow), the phone's time zone, What's it about? (optional) and Book call — then Booked — Asha will call you on Thu 16 Oct, 6:00 PM with Done. Your app opens it and gets the booked call back. New in 0.8.0 on every platform (React Native 0.42.0).

The booking sheet on an iPhone with the day chips, the time wheel, the time zone, What's it about and Book call; the same sheet after booking, saying when the agent will call; and the sheet on Android over the app's own calls screen

What it needs in the dashboard

WhereSettingValue
Agent → Calls & scheduling → Who may book a callUsers, from your appOn, with At most this many upcoming calls per user, Booked at most this far ahead, With at least this much notice
Agent → Calling WindowThe hours the agent may ringA booked time outside them is refused
Workspace settings → In-app callingiOS and Android credentialsChecked and working. — Graine must be able to ring the phone (rings)
Agent → Memory → SettingsWho is the user?Bookings are for the identified user
The Garage → Chat tabBooked callsShown, for the chat's own Book a call

What to do, in order

  1. Configure the SDK, and on iOS enable VoIP pushes at launch — booking needs Graine to be able to ring this phone (rings).
  2. Identify the user after sign-in.
  3. Read bookingAvailable and show your Call me button only when it is true. It is true when all three hold: Graine rings this phone (In-app calling is set up for the platform), the agent lets users book, and the user is identified. On a website only the last two count. Read it again after identify and once the agent's settings have loaded.
  4. Open the sheet with an optional reason, which fills What's it about? → It returns the booked call when the sheet closes — or nothing when the user closed it without booking. A refusal stays in the sheet as one line under the button (below); the user can change the time and try again.
  5. Refresh your own list of calls if you show one. The chat window's upcoming-calls card refreshes itself and offers Book a call too.
  6. On sign-out identify(null): bookingAvailable turns false.
RefusalThe line the sheet shows
too_soonPick a time at least 5 minutes from now (the agent's notice) — the sheet moves to the new earliest time
too_farPick a time within 14 days (the agent's limit)
outside_windowThat time is outside the hours we can call — pick another
too_many_pendingYou already have 3 calls booked — cancel one first (the agent's limit)
scheduling_offBooking a call isn't available right now
identity_requiredSign in to book a call
anything else (rate_limited, unavailable, network)Couldn't book just now — try again

The counts are the agent's own settings; one is singular (1 minute, 1 day, 1 call). On a website the sheet adds We'll ring you in the app: a browser cannot be rung.

The code, platform by platform

1 and 2. GraineInApp.configure(…), calls.enableVoIPPushes() at launch; identify(["user_id": user.id]) after sign-in.

3 and 4. A button that opens the sheet — SwiftUI:

struct CallMeButton: View {
    @State private var available = false
    @State private var booking = false
    var body: some View {
        Group {
            if available { Button("Call me") { booking = true } }
        }
        .onAppear { available = GraineInApp.shared.bookingAvailable }
        .graineBookingSheet(isPresented: $booking, reason: "Talk through my order") { call in
            print("Booked \(call.id) for \(call.at?.description ?? "")")   // the moment it is booked
        }
    }
}

From anywhere, async — over the key window's top view controller:

if GraineInApp.shared.bookingAvailable {
    if let call = await GraineInApp.shared.presentBooking(reason: "Talk through my order") {
        print(call.id, call.status, call.canCancel)                    // scheduled, true
    } else {
        print("Closed without booking")
    }
}

UIKit: present GraineBookingViewController(reason:done:); your own presentation: GraineBookingView(onClose:). Objective-C: the agent's presentBookingWithReason:completion: on GraineObjC, and its bookingAvailable.

5. Refresh your list: let calls = try await GraineInApp.shared.scheduledCalls().

What comes back

The booked call, as GraineScheduledCall:

{
  "id": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35", "at": "2026-10-16T12:30:00Z", "status": "scheduled",
  "source": "app", "reason": "Talk through my order", "isUpcoming": true, "canCancel": true
}

at is a moment (UTC on the wire); show it in the device's time zone. When it is due, Graine rings the user's newest phone — Calls Graine rings.

Troubleshooting

What you seeWhyFix
bookingAvailable is falseOne of the three rules failsUsers, from your app on; In-app calling Checked and working. for this platform; identify with the identifier
The sheet opens and says Sign in to book a callOpened while the user is not identifiedCheck bookingAvailable before offering it
That time is outside the hours we can callThe agent's Calling Window, in the user's time zonePick a time inside it, or widen the window
No Book a call in the chatBooking unavailable, or Booked calls hidden in the GarageAs above; Chat tab → Booked calls shown

The rules every booking follows: Scheduled calls · SDK reference → Booking a call.

On this page