Building an app card
What a card is
A card is a small panel your app supplies for a SHOTA page, shown only to signed-in users who have connected your app: for example their QSL status with your service, or a summary of their account. You send the content as JSON in the open Adaptive Cards format; we check it and draw it in our own style. Nothing you send runs on our pages.
Register a card URL for each slot on your app (My apps β Edit). We review it, and we decide which apps can use each slot (the card:{slot} permission). Use Preview on your app's page to test at any time, even before it's enabled.
Slots
dashboard: My Shack dashboard (about 350 pixels wide)logs: Logs & uploads page (about 350 pixels wide)
Up to 3 cards can share a slot. Every card is labelled "From" your app's name and logo, and users can see why they're seeing it.
Our request
When a user opens the page, our server (never their browser) calls your card URL:
GET {your card URL}?slot=dashboard
Authorization: Bearer {signed token}
Accept: application/json
User-Agent: SHOTA-Cards/1.0 The token is a JWT signed with RS256. Always verify it before answering, using our public keys at https://shacksontheair.com/.well-known/jwks.json. Check that:
issishttps://shacksontheair.comaudis your client IDexphas not passed (tokens last 60 seconds)subis the user's account ID, the sameidthatGET /api/partner/v1/mereturns. Show only that user's data.slotis the slot being filled- the token header's
typiscard+jwt. Our OAuth access tokens are signed with the same key, so this is what tells a card request apart from an access token. Never accept one for the other.
We send nothing else about the user or the page they're on.
Your response
Answer 200 with an Adaptive Card as JSON, or 204 to show nothing for this user. For example:
{
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": "Your QSLs this month", "weight": "bolder", "size": "medium" },
{ "type": "FactSet", "facts": [
{ "title": "Sent", "value": "24" },
{ "title": "Confirmed", "value": "19" }
] },
{ "type": "TextBlock", "text": "**All caught up**", "isSubtle": true }
],
"actions": [
{ "type": "Action.OpenUrl", "title": "Open My QSLs", "url": "https://example.com/account" }
]
} What you can use
| Element | Supported properties |
|---|---|
| TextBlock | text, size (small, default, medium, large), weight (lighter, default, bolder), color (default, dark, light, accent, good, warning, attention), isSubtle, horizontalAlignment, maxLines, separator. Text may use **bold**, _italic_ and [links](https://...). |
| Image | url, altText, size (auto, small, medium, large, stretch), style (person for round), horizontalAlignment |
| FactSet | facts: up to 20 of { "title", "value" } |
| ColumnSet | columns: up to 4, each with width (auto, stretch or a weight) and items |
| Container | items, style (default, emphasis, good, attention, warning, accent) |
| ActionSet, actions | Action.OpenUrl only, with title and url; up to 5 |
Anything else (inputs, submit actions, other element types or properties) is left out, as the Adaptive Cards specification allows. The Adaptive Cards Designer is handy for laying out your card.
Limits
- Answer within 3 seconds and in at most 50 KB, uncompressed, or the card is left out.
- Redirects are not followed. Your card URL must be https, on port 443, on a host that resolves to public internet addresses only.
- Links must be https. Images must be https and on your card URL's domain (or its subdomains).
- We keep each user's card for 2 minutes, so your server isn't called on every page view. A card that fails is retried after a minute; an app that keeps failing is paused for a few minutes.
- At most 60 elements, nested at most 6 deep.
What cards may show
Cards are for the connected user's own data from your service. They must not advertise other products, ask for payment or donations, ask for passwords or other credentials, or look like they come from Shacks On The Air. See the Developer Terms. We can switch off any app's card at once.