Sign in (OAuth 2.0)
Authenticate a customer inside an Apple Messages for Business conversation with Apple's native Sign in sheet and your OAuth 2.0 identity provider, then exchange the authorization code server-side.
Sometimes you need to know who you're talking to before you can help — look up an order, change an account setting, or share something private. Apple Messages for Business lets a customer authenticate inside the thread: you send a Sign in request, Apple presents a native sign-in sheet backed by your OAuth 2.0 identity provider, and you receive an authorization code to exchange server-side. The customer never leaves Messages.
This is the authenticate variant of an interactive message. It is sent as Apple's New Authentication (interactive data version 2.0).
Requirements
- OAuth integration configured. Set up your OAuth 2.0 provider on the channel in the dashboard under Integrations → Apple Messages for Business → OAuth. A send fails fast if the integration is missing.
- Device support. New Authentication (OAuth 2.0) requires iOS/iPadOS 16 or macOS 13 and advertises the
AUTH2capability token. Check the device'scapability-listbefore sending; a device that doesn't advertiseAUTH2is rejected with422 amb_capability_unavailable. - Your own IdP. The customer signs in against your authorization server — Blooio does not host an identity provider.
Send a Sign in request
Send an interactive message with kind: "authenticate". Put the OAuth 2.0 request under oauth2 (a shorthand for authenticate.oauth2); it is forwarded to Apple verbatim, so it uses Apple's field names.
curl -X POST https://api.blooio.com/v4/messages \
-H "Authorization: Bearer bl_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4d5e6f70-8a9b-4c1d-9e2f-3a4b5c6d7e8f" \
-d '{
"from": "support",
"to": "opaque-amb-customer-id",
"interactive": {
"kind": "authenticate",
"received_message": {
"title": "Sign in to your account",
"subtitle": "Verify it's you to continue",
"style": "icon"
},
"reply_message": { "title": "Signed in", "style": "icon" },
"oauth2": {
"responseType": "code",
"scope": ["openid", "profile", "email"],
"redirectURI": "https://auth.yourbrand.com/amb/callback",
"state": "order-10482-7f3a9c"
}
}
}'Try itFields
| Field | Type | What it is |
|---|---|---|
interactive.kind |
string | Required. "authenticate". |
oauth2.responseType |
string | Required. The OAuth 2.0 response type — use "code" for the authorization-code flow. |
oauth2.scope |
array of string | Required. The scopes to request (e.g. openid, profile, email). |
oauth2.redirectURI |
string (https) | Required. Your registered redirect URI. Must match what your authorization server expects. |
oauth2.state |
string | Recommended. An opaque value you generate; echoed back so you can correlate the reply to this request and defend against CSRF. |
received_message |
object | The bubble the customer sees before signing in (title, subtitle, style, optional imageIdentifier). |
reply_message |
object | The bubble shown after they finish. |
Note You may also nest the request under
authenticate("authenticate": { "oauth2": { … } }) instead of the flatoauth2shorthand — both reach the wire. The request is validated for the minimum Apple needs (responseType,scope,redirectURI) before sending; a malformed request is rejected with422 invalid_authenticatenaming the missing field, rather than delivering an empty bubble.
The sign-in round trip: your send, Apple's sheet, the customer authenticating against your IdP, and the authorization code arriving back on an inbound webhook. Click a step for details.
What the customer sees
- A Sign in bubble appears with your
received_messagetitle/subtitle. - Tapping it opens Apple's native authentication sheet, which loads your authorization server's sign-in page for the requested
scope. - The customer authenticates. Your server redirects to
redirectURIwith an authorizationcode(and yourstate). - The sheet closes and the
reply_messagebubble is shown in the thread.
Reading the result
The outcome arrives as a normal inbound message webhook, with a structured metadata.interactive object carrying the authorization code and your state:
{
"role": "reply",
"kind": "authenticate",
"code": "authorization-code-from-your-idp",
"state": "order-10482-7f3a9c"
}Then, server-side, exchange that code for tokens at your own token endpoint — exactly as in any OAuth 2.0 authorization-code flow:
curl -X POST https://auth.yourbrand.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=authorization-code-from-your-idp" \
-d "redirect_uri=https://auth.yourbrand.com/amb/callback" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"Verify the returned state matches the one you sent before trusting the code. The access token your IdP returns identifies the customer — use it to look up their account and continue the conversation with confidence.
Note The code is single-use and short-lived. Exchange it promptly, and never do the exchange from a client — keep your
client_secretserver-side.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
422 invalid_authenticate |
The oauth2 object is missing responseType, scope, or redirectURI. The message names the field. |
422 amb_capability_unavailable |
The customer's latest device doesn't advertise AUTH2 (needs iOS 16 / macOS 13+). Fall back to a different flow or a link. |
| The sheet opens but sign-in fails | Your authorization server rejected the request — confirm the redirectURI is registered and the scope values are valid for your client. |
| No reply webhook arrives | Confirm your webhook endpoint is reachable and subscribed to message events; see Receive webhooks locally. |