SGIPv0.1

Reference

Operations

Every operation travels in the same envelope. A platform declares the ops it answers in its capabilities; the hub routes only to platforms that declared them. This page is generated from the schemas in spec/schemas/ops.

Joining

hub.hello@1

Platform โ†’ HubRequired

Register or update a platform. The hub checks https://<platform_id>/.well-known/sgip.json lists the same key.

Payload

FieldTypeNotes
namerequiredstringmax 60 characters
iconstringmax 8 characters
inboxrequiredstring
visitstring
public_keyrequiredstring
capabilitiesrequiredop[]

Result

FieldTypeNotes
registeredrequiredobject
platform_idrequiredplatformId
hub_public_keyrequiredstring
Example
payload
{
  "name": "Berlin Days",
  "icon": "๐Ÿป",
  "inbox": "https://berlin.example/sgip/inbox",
  "visit": "https://berlin.example/sgip/visit",
  "public_key": "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=",
  "capabilities": [
    "hub.ping@1",
    "player.lookup@1",
    "message.send@1",
    "visit.end@1"
  ]
}
result
{
  "registered": true,
  "platform_id": "berlin.example",
  "hub_public_key": "1A4Pun+lbVo1w72cjWIq2WRNyVw+1DT4PehBGHINEts="
}

hub.ping@1

BothRequired

Health check, in either direction.

Payload

An empty object.

Result

FieldTypeNotes
platformrequiredstring
timerequiredtimestamp
Example
payload
{}
result
{
  "platform": "berlin.example",
  "time": "2026-10-09T12:00:00Z"
}

hub.check@1

Platform โ†’ Hub

Platform โ†’ Hub. Ask the hub to test this platform against the contract (section 9.1). Signed with the platform's own key; only the platform can ask for its own check.

Payload

An empty object.

Result

FieldTypeNotes
conformsrequiredboolean
checksrequiredobject[]
checks[].namerequiredstring
checks[].passedrequiredboolean
checks[].requiredrequiredboolean
checks[].detailrequiredstring
Example
payload
{}
result
{
  "conforms": true,
  "checks": [
    {
      "name": "Descriptor is served",
      "passed": true,
      "required": true,
      "detail": "https://berlin.example/.well-known/sgip.json"
    },
    {
      "name": "Answers a hub-signed hub.ping@1",
      "passed": true,
      "required": true,
      "detail": ""
    },
    {
      "name": "Rejects a replayed nonce",
      "passed": true,
      "required": true,
      "detail": ""
    },
    {
      "name": "Visit page loads",
      "passed": true,
      "required": false,
      "detail": "https://berlin.example/sgip/visit answered 200."
    }
  ]
}

platform.directory@1

Platform โ†’ Hub

List registered platforms so a UI can hide buttons a target cannot handle.

Payload

An empty object.

Result

FieldTypeNotes
platformsrequiredobject[]
platforms[].platform_idrequiredplatformId
platforms[].namerequiredstring
platforms[].iconstring | null
platforms[].icon_urlstring | null
platforms[].capabilitiesrequiredop[]
platforms[].visit_urlstring | null
platforms[].return_urlstring | null
platforms[].originstring | null
platforms[].embedobject
platforms[].embed.allowedboolean
platforms[].currenciescurrency[]
platforms[].acceptsobject[]The money this game accepts from visitors (section 7.4)
platforms[].accepts[].fromrequiredplatformId
platforms[].accepts[].giverequiredmoneyIn the home currency
platforms[].accepts[].getrequiredmoneyWhat `give` buys here, in this game's currency
platforms[].accepts[].daily_inflow_limitrequiredmoneyThe most that may enter from this game per UTC day, in `get`'s currency
platforms[].visitors_welcomebooleanWhether players from the game asking may visit this one (section 7.5)
Example
payload
{}
result
{
  "platforms": [
    {
      "platform_id": "lagos.example",
      "name": "Lagos Stories",
      "icon": "๐ŸŒด",
      "capabilities": [
        "message.send@1",
        "visit.end@1"
      ],
      "visit_url": "https://lagos.example/sgip/visit",
      "origin": "https://lagos.example",
      "embed": {
        "allowed": true
      },
      "visitors_welcome": true
    }
  ]
}

Players

player.lookup@1

Platform โ†’ Hub โ†’ PlatformRequired

Public profile of a discoverable player. not_found for players who have not opted in.

Payload

FieldTypeNotes
playerrequiredplayerAddress

Result

FieldTypeNotes
playerrequiredplayerAddress
subrequiredstring
display_namerequiredstring
avatarobject
badgesstring[]
Example
payload
{
  "player": "lena@berlin.example"
}
result
{
  "player": "lena@berlin.example",
  "sub": "01JA00000000000000000000SB",
  "display_name": "Lena from Kreuzberg",
  "avatar": {
    "color": "#e76f51"
  },
  "badges": [
    "newcomer"
  ]
}

player.profile@1

Platform โ†’ Hub โ†’ Platform

A player's public card: what other games may show about them. Platforms MUST answer not_found for players who are not discoverable, exactly as player.lookup@1 does.

Payload

FieldTypeNotes
playerrequiredplayerAddress

Result

FieldTypeNotes
playerrequiredplayerAddress
displayrequireddisplay
biostringmax 280 characters
sincetimestamp
careerobject
career.fieldcareerField
career.titlerequiredstringmax 60 characters
career.levelinteger
career.top_levelinteger
skillsobject[]
skills[].skillrequiredskill
skills[].levelrequiredinteger0โ€“10
homestringThe district they live in, never an exact home address.. max 60 characters
presencepresence
Example
payload
{
  "player": "lena@berlin.example"
}
result
{
  "player": "lena@berlin.example",
  "display": {
    "name": "Lena from Kreuzberg",
    "color": "#e76f51",
    "badges": [
      "Newcomer"
    ]
  },
  "since": "2026-10-08T22:09:50Z",
  "career": {
    "field": "technology",
    "title": "Developer",
    "level": 1,
    "top_level": 4
  },
  "skills": [
    {
      "skill": "coding",
      "level": 4
    },
    {
      "skill": "music",
      "level": 2
    }
  ],
  "home": "Kreuzberg",
  "presence": "online"
}

presence.update@1

Platform โ†’ Hub โ†’ subscribersQueued if offline

A player's presence changed. Only sent for discoverable players; the hub fans it out to subscribers.

Payload

FieldTypeNotes
playerrequiredplayerAddress
statusrequiredpresence
activityobjectWhat they are doing, if they share it.
activity.verbrequiredverb
activity.labelstringmax 60 characters
activity.placeplaceAddress
activity.posepose

Result

An empty object.

Example
payload
{
  "player": "lena@berlin.example",
  "status": "online",
  "activity": {
    "verb": "eat",
    "label": "Currywurst at the Imbiss",
    "pose": "stand"
  }
}
result
{}

presence.subscribe@1

Platform โ†’ Hub

Receive presence.update@1 for these players.

Payload

FieldTypeNotes
playersrequiredplayerAddress[]

Result

FieldTypeNotes
subscribedrequiredinteger
Example
payload
{
  "players": [
    "tunde@lagos.example"
  ]
}
result
{
  "subscribed": 1
}

Messages and invites

message.send@1

Platform โ†’ Hub โ†’ PlatformRequiredQueued if offline

A direct message between players. Envelope from.player and to.player are required.

Payload

FieldTypeNotes
textrequiredplainText
from_namestringmax 60 characters
langstringmax 12 characters

Result

An empty object.

Example
payload
{
  "text": "How far? Greetings from Lagos",
  "from_name": "Tunde",
  "lang": "en"
}
result
{
  "delivered": true
}

message.receipt@1

Platform โ†’ Hub โ†’ PlatformQueued if offline

Delivered or read receipts for messages the recipient got.

Payload

FieldTypeNotes
message_idsrequiredulid[]
statusrequired"delivered" | "read"

Result

An empty object.

Example
payload
{
  "message_ids": [
    "01JA0000000000000000000011"
  ],
  "status": "read"
}
result
{
  "updated": 1
}

invite.send@1

Platform โ†’ Hub โ†’ PlatformQueued if offline

Invite a player to visit the sender's game.

Payload

FieldTypeNotes
invite_idrequiredulid
from_namestringmax 60 characters
hostrequiredplatformId
placeplaceAddress
notestringmax 200 characters
expires_attimestamp

Result

An empty object.

Example
payload
{
  "invite_id": "01JA00000000000000000000N1",
  "from_name": "Lena",
  "host": "berlin.example",
  "place": "place://berlin.example/mitte/hauptbahnhof",
  "note": "Currywurst is on me",
  "expires_at": "2026-10-10T12:00:00Z"
}
result
{
  "received": true
}

invite.respond@1

Platform โ†’ Hub โ†’ PlatformQueued if offline

Accept or decline an invite.

Payload

FieldTypeNotes
invite_idrequiredulid
acceptedrequiredboolean

Result

An empty object.

Example
payload
{
  "invite_id": "01JA00000000000000000000N1",
  "accepted": true
}
result
{}

Visits

visit.request@1

Home โ†’ Hub

Get (or refresh, with the same visit_id) a passport and launch URL for a host platform.

Payload

FieldTypeNotes
playerrequiredplayerAddress
subrequiredstringmax 64 characters
hostrequiredplatformId
visit_idulid
arrivalplaceAddress
scopesscope[]
displaydisplay
budgetmoneySent with the pay scope: the most the host may charge on this visit, in a home currency

Result

FieldTypeNotes
visit_idrequiredulid
passportrequiredstring
launch_urlrequiredstring
host_originstring
host_namestring
expires_atrequiredintegerWhen the passport expires, in Unix seconds: its `exp` claim
embedobject
embed.allowedboolean
Example
payload
{
  "player": "tunde@lagos.example",
  "sub": "01JA00000000000000000000SB",
  "host": "berlin.example",
  "scopes": [
    "visit",
    "chat",
    "act"
  ],
  "display": {
    "name": "Tunde",
    "color": "#2e4a6b"
  }
}
result
{
  "visit_id": "01JA00000000000000000000V1",
  "passport": "eyJhbGciOiJFZERTQSJ9.e30.c2ln",
  "launch_url": "https://berlin.example/sgip/visit",
  "host_origin": "https://berlin.example",
  "host_name": "Berlin Days",
  "expires_at": 1791480900,
  "embed": {
    "allowed": true
  }
}

visit.end@1

Host โ†’ Hub โ†’ HomeQueued if offline

The visit is over; the home game takes the player back and caps each souvenir at ยฑ20.

Payload

FieldTypeNotes
visit_idrequiredulid
playerrequiredplayerAddress
reasonrequired"left" | "idle" | "removed" | "expired"
duration_sinteger
minutesintegerGame minutes that passed for the guest
souvenirsstatChanges
last_placeplaceAddress
summarystringmax 200 characters
activitiesobject[]What the guest did, by verb, so the home game can tell the story.
activities[].verbrequiredverb
activities[].countrequiredinteger

Result

An empty object.

Example
payload
{
  "visit_id": "01JA00000000000000000000V1",
  "player": "tunde@lagos.example",
  "reason": "left",
  "duration_s": 640,
  "minutes": 120,
  "souvenirs": {
    "mood": 12,
    "hunger": -8
  },
  "last_place": "place://berlin.example/kreuzberg/kneipe-am-kanal",
  "activities": [
    {
      "verb": "drink",
      "count": 1
    },
    {
      "verb": "explore",
      "count": 2
    }
  ]
}
result
{
  "returned": true
}

travel.quote@1

Platform โ†’ Hub โ†’ Platform

Where a traveller would arrive in the host game, and how long it takes. The fare is charged by the game the player leaves.

Payload

FieldTypeNotes
moderequiredtransportMode
fromplaceAddress

Result

FieldTypeNotes
moderequiredtransportMode
arrivalrequiredplaceAddress
arrival_namestring
duration_minutesinteger
gatewaysobject[]
Example
payload
{
  "mode": "flight",
  "from": "place://lagos.example/ikeja/airport"
}
result
{
  "mode": "flight",
  "arrival": "place://berlin.example/mitte/hauptbahnhof",
  "arrival_name": "Berlin Hauptbahnhof",
  "duration_minutes": 420
}

The world

world.places@1

Platform โ†’ Hub โ†’ Platform

A platform's public places.

Payload

An empty object.

Result

FieldTypeNotes
placesrequiredobject[]
places[].idrequiredplaceAddress
places[].namerequiredstring
places[].districtstring
places[].kindrequiredplaceKind
places[].latnumber
places[].lngnumber
places[].gatewayboolean
places[].guests_allowedboolean
places[].capacityinteger
places[].hoursarray | null
places[].osmstringThe OpenStreetMap element it came from, like node/1504407936.
Example
payload
{}
result
{
  "places": [
    {
      "id": "place://berlin.example/kreuzberg/kneipe-am-kanal",
      "name": "Kneipe am Kanal",
      "district": "kreuzberg",
      "kind": "pub",
      "lat": 52.4966,
      "lng": 13.4244,
      "gateway": false,
      "guests_allowed": true,
      "capacity": 60,
      "hours": [
        16,
        26
      ]
    }
  ]
}

place.status@1

Platform โ†’ Hub โ†’ Platform

How a place is right now: open or shut, how busy, how many players are there. Cheap to answer; meant for "is it worth going?" before a visit.

Payload

FieldTypeNotes
placerequiredplaceAddress

Result

FieldTypeNotes
placerequiredplaceAddress
kindplaceKind
openrequiredboolean
busynessnumber0 empty, 1 packed.. 0โ€“1
playersintegerPlayers there now, counting visitors. A count only, never who.
opens_attimestamp
closes_attimestamp
eventsstring[]Ids of world.events@1 events on now.
Example
payload
{
  "place": "place://berlin.example/neukoelln/stadtbad"
}
result
{
  "place": "place://berlin.example/neukoelln/stadtbad",
  "kind": "swimming_pool",
  "open": true,
  "busyness": 0.6,
  "players": 3,
  "closes_at": "2026-10-09T20:00:00Z"
}

world.events@1

Platform โ†’ Hub โ†’ Platform

What is on at a platform: concerts, matches, market days, weddings. Lets other games show "what's on in Berlin tonight" and invite players to it.

Payload

FieldTypeNotes
fromtimestamp
totimestamp
kindeventKind
limitinteger1โ€“100

Result

FieldTypeNotes
eventsrequiredobject[]
events[].idrequiredstringmax 64 characters
events[].kindrequiredeventKind
events[].titlerequiredstringmax 80 characters
events[].descriptionstringmax 500 characters
events[].placerequiredplaceAddress
events[].starts_atrequiredtimestamp
events[].ends_attimestamp
events[].pricemoney
events[].verbverb
events[].gamegameKind
events[].guests_allowedbooleanWhether visitors from other games may attend.
Example
payload
{
  "kind": "match"
}
result
{
  "events": [
    {
      "id": "berlin-derby",
      "kind": "match",
      "title": "The Berlin derby",
      "place": "place://berlin.example/koepenick/stadium",
      "starts_at": "2026-10-10T15:30:00Z",
      "ends_at": "2026-10-10T17:30:00Z",
      "price": {
        "amount": 2500,
        "currency": "EUR"
      },
      "verb": "watch",
      "guests_allowed": true
    }
  ]
}

action.list@1

Platform โ†’ Hub โ†’ Platform

What a guest can do at a place, in shared verbs and stats.

Payload

FieldTypeNotes
placerequiredplaceAddress

Result

FieldTypeNotes
placerequiredplaceAddress
actionsrequiredobject[]
actions[].idrequiredstring
actions[].verbrequiredverb
actions[].labelrequiredstring
actions[].duration_minutesrequiredinteger
actions[].pricerequiredmoney
actions[].paysmoney | null
actions[].effectsrequiredstatChanges
actions[].tagsverb[]More verbs that also describe it (a pub quiz: play, talk).
actions[].posepose
actions[].skillsobjectSkill points it trains.
actions[].interactioninteraction
actions[].gamegameKind
actions[].guests_allowedbooleanHosts SHOULD list only what guests may do; this flags the rest when they are shown.
Example
payload
{
  "place": "place://berlin.example/kreuzberg/kneipe-am-kanal"
}
result
{
  "place": "place://berlin.example/kreuzberg/kneipe-am-kanal",
  "actions": [
    {
      "id": "beer",
      "verb": "drink",
      "label": "Glass of Berliner Pils",
      "duration_minutes": 40,
      "price": {
        "amount": 450,
        "currency": "EUR"
      },
      "pays": null,
      "effects": {
        "mood": 10,
        "social": 15,
        "energy": -5
      },
      "pose": "sit",
      "tags": [
        "talk"
      ]
    }
  ]
}

Money

wallet.balance@1

Host โ†’ Hub

Host โ†’ Hub. What a visitor can spend, in each of the host's currencies that has a rate for the budget's currency (section 7.4).

Payload

FieldTypeNotes
playerrequiredplayerAddress
visit_idrequiredulid

Result

FieldTypeNotes
spendablerequiredmoney[]
Example
payload
{
  "player": "tunde@lagos.example",
  "visit_id": "01JA00000000000000000000V1"
}
result
{
  "spendable": [
    {
      "amount": 10000,
      "currency": "EUR"
    }
  ]
}

wallet.charge@1

Host โ†’ Hub

Host โ†’ Hub. Charge a visitor's home money for something they chose. The envelope id is the charge id (section 7.4).

Payload

FieldTypeNotes
playerrequiredplayerAddress
visit_idrequiredulid
amountrequiredmoneyIn one of the host's currencies
descriptionrequiredstringmax 120 characters
verbverb

Result

FieldTypeNotes
charge_idrequiredulid
chargedrequiredmoney
debitedrequiredmoneyWhat the home game took, in its currency
spendablerequiredmoney[]
Example
payload
{
  "player": "tunde@lagos.example",
  "visit_id": "01JA00000000000000000000V1",
  "amount": {
    "amount": 450,
    "currency": "EUR"
  },
  "description": "Glass of Berliner Pils",
  "verb": "drink"
}
result
{
  "charge_id": "01JA2B3C4D5E6F7G8H9J0K1M2N",
  "charged": {
    "amount": 450,
    "currency": "EUR"
  },
  "debited": {
    "amount": 225000,
    "currency": "NGN"
  },
  "spendable": [
    {
      "amount": 9550,
      "currency": "EUR"
    }
  ]
}

wallet.refund@1

Host โ†’ Hub

Host โ†’ Hub. Return all or part of a charge within 24 hours. Without an amount, returns whatever remains (section 7.4).

Payload

FieldTypeNotes
charge_idrequiredulid
amountmoneyIn the charge's currency; at most what remains unrefunded
reasonstringmax 120 characters

Result

FieldTypeNotes
refund_idrequiredulid
refundedrequiredmoney
creditedrequiredmoneyWhat the home game returned, in its currency
Example
payload
{
  "charge_id": "01JA2B3C4D5E6F7G8H9J0K1M2N",
  "amount": {
    "amount": 450,
    "currency": "EUR"
  },
  "reason": "Closed before it was served"
}
result
{
  "refund_id": "01JA2B3C4D5E6F7G8H9J0K1M3P",
  "refunded": {
    "amount": 450,
    "currency": "EUR"
  },
  "credited": {
    "amount": 225000,
    "currency": "NGN"
  }
}

wallet.read@1

Hub โ†’ Home

Hub โ†’ Home. A player's balance in one currency (section 7.4).

Payload

FieldTypeNotes
playerrequiredplayerAddress
currencyrequiredcurrency

Result

FieldTypeNotes
balancerequiredmoney
Example
payload
{
  "player": "tunde@lagos.example",
  "currency": "NGN"
}
result
{
  "balance": {
    "amount": 50000000,
    "currency": "NGN"
  }
}

wallet.debit@1

Hub โ†’ Home

Hub โ†’ Home. Take an amount if the player has it, in one step, or answer insufficient_funds. A repeated charge_id is the same debit; a charge_id already voided by wallet.credit@1 is refused (section 7.4).

Payload

FieldTypeNotes
charge_idrequiredulid
playerrequiredplayerAddress
amountrequiredmoney
hostrequiredplatformId
host_amountrequiredmoney
descriptionrequiredstringmax 120 characters

Result

FieldTypeNotes
balancerequiredmoney
Example
payload
{
  "charge_id": "01JA2B3C4D5E6F7G8H9J0K1M2N",
  "player": "tunde@lagos.example",
  "amount": {
    "amount": 225000,
    "currency": "NGN"
  },
  "host": "berlin.example",
  "host_amount": {
    "amount": 450,
    "currency": "EUR"
  },
  "description": "Glass of Berliner Pils"
}
result
{
  "balance": {
    "amount": 49775000,
    "currency": "NGN"
  }
}

wallet.credit@1

Hub โ†’ Home

Hub โ†’ Home. Return a refunded or voided amount. A repeated refund_id is the same credit. For a charge_id never debited, record it and refuse any later debit with that id (section 7.4).

Payload

FieldTypeNotes
charge_idrequiredulid
refund_idrequiredulid
playerrequiredplayerAddress
amountrequiredmoney

Result

FieldTypeNotes
balancerequiredmoney
Example
payload
{
  "charge_id": "01JA2B3C4D5E6F7G8H9J0K1M2N",
  "refund_id": "01JA2B3C4D5E6F7G8H9J0K1M3P",
  "player": "tunde@lagos.example",
  "amount": {
    "amount": 225000,
    "currency": "NGN"
  }
}
result
{
  "balance": {
    "amount": 50000000,
    "currency": "NGN"
  }
}