{
  "openapi": "3.1.0",
  "info": {
    "title": "Orbweave API",
    "version": "1.1.0",
    "description": "Server-to-server API for BlackCrystal's games, sponsor consoles and partners. Every source authenticates with its own secret (Bearer or HMAC). Writes are idempotent on the caller's reference ids: repeating a call returns {ok:true, duplicate:true}. The Weaver phone app uses /api/v1/app/* with per-device Weaver tokens instead."
  },
  "servers": [
    {
      "url": "https://api.orbweave.xyz"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer <source secret> (generated in HQ \u2192 Settings \u2192 App secrets)."
      },
      "hmac": {
        "type": "apiKey",
        "in": "header",
        "name": "X-OW-Signature",
        "description": "hex(HMAC-SHA256(secret, METHOD\\nPATH\\nX-OW-Timestamp\\nBODY)) with X-OW-Source and X-OW-Timestamp (ms, 5-minute window)."
      }
    },
    "schemas": {
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "duplicate": {
            "type": "boolean"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "Business": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "contact_name": {
            "type": "string"
          },
          "contact_phone": {
            "type": "string"
          },
          "contact_email": {
            "type": "string"
          },
          "town": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2, default AU"
          }
        }
      },
      "Sponsor": {
        "type": "object",
        "properties": {
          "sponsorship_id": {
            "type": "integer"
          },
          "business": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "offer_text": {
            "type": "string"
          },
          "ends_at": {
            "type": "string"
          },
          "town": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "weaver_code": {
            "type": "string"
          }
        }
      }
    }
  },
  "security": [
    {
      "bearer": []
    },
    {
      "hmac": []
    }
  ],
  "paths": {
    "/api/v1/ping": {
      "get": {
        "summary": "Connectivity check",
        "description": "Returns the source the secret belongs to.",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/v1/attribution": {
      "post": {
        "summary": "Attribute an install to a code / Weaver",
        "description": "Call at signup with the ow code the install carried, and again at the activity gate (pays the Weaver's bounty; falls back to the signup's Weaver when no code is sent). App secrets only.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "install_ref",
                  "event"
                ],
                "properties": {
                  "install_ref": {
                    "type": "string",
                    "description": "stable per-install id"
                  },
                  "event": {
                    "type": "string",
                    "enum": [
                      "signup",
                      "activity_gate"
                    ]
                  },
                  "code": {
                    "type": "string",
                    "description": "ow code from the sticker / link"
                  },
                  "weaver_code": {
                    "type": "string"
                  },
                  "town": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ok, attribution_id, weaver_id, web_id, credited}"
          }
        }
      }
    },
    "/api/v1/sponsors": {
      "get": {
        "summary": "Live local sponsors for this app",
        "description": "By point (?lat&lng&radius_km, default 25 km) or by town (?town&state&country). Returns offer_text for the in-game Local deals list.",
        "parameters": [
          {
            "name": "lat",
            "in": "query"
          },
          {
            "name": "lng",
            "in": "query"
          },
          {
            "name": "radius_km",
            "in": "query"
          },
          {
            "name": "town",
            "in": "query"
          },
          {
            "name": "state",
            "in": "query"
          },
          {
            "name": "country",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, count, sponsors: Sponsor[]}"
          }
        }
      },
      "post": {
        "summary": "A sponsor paid (sponsor console \u2192 Orbweave)",
        "description": "One call per payment with a unique external_ref (invoice / charge id). Attribution: code > weaver_code > the town's claimed Weaver. Flips the lead to live and credits commission. v0.3.0: also accepts band ('city'|'small') and list_price_cents \u2014 the band the console priced at and the list price, so price shown = price charged can be audited.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_ref",
                  "amount_cents",
                  "business"
                ],
                "properties": {
                  "external_ref": {
                    "type": "string"
                  },
                  "amount_cents": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string",
                    "default": "AUD"
                  },
                  "pack": {
                    "type": "string"
                  },
                  "offer_text": {
                    "type": "string"
                  },
                  "starts_at": {
                    "type": "string"
                  },
                  "ends_at": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "weaver_code": {
                    "type": "string"
                  },
                  "business": {
                    "$ref": "#/components/schemas/Business"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ok, sponsorship_id, business_id, weaver_id, web_id, credited}"
          }
        }
      }
    },
    "/api/v1/sponsors/refund": {
      "post": {
        "summary": "A sponsor payment was refunded",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_ref"
                ],
                "properties": {
                  "external_ref": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ok, sponsorship_id}"
          }
        }
      }
    },
    "/api/v1/redemptions": {
      "post": {
        "summary": "A player redeemed a sponsor offer",
        "description": "staff_confirmed=true when staff confirmed in-app (PIN / tap); otherwise confirm later. Only confirmed redemptions pay the Weaver bonus.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sponsorship_id"
                ],
                "properties": {
                  "sponsorship_id": {
                    "type": "integer"
                  },
                  "player_ref": {
                    "type": "string"
                  },
                  "external_ref": {
                    "type": "string"
                  },
                  "offer_value_cents": {
                    "type": "integer"
                  },
                  "staff_confirmed": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ok, redemption_id, credited}"
          }
        }
      }
    },
    "/api/v1/redemptions/{id}/confirm": {
      "post": {
        "summary": "Staff confirmation for an earlier redemption",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, redemption_id, credited}"
          }
        }
      }
    },
    "/api/v1/weavers/{code}": {
      "get": {
        "summary": "Validate a Weaver code",
        "description": "For the 'Who signed you up?' field in sponsor consoles and storefront checkout.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, code, display_name, rank, home_town, home_state}"
          },
          "404": {
            "description": "unknown or inactive"
          }
        }
      }
    },
    "/api/v1/sales": {
      "post": {
        "summary": "An app was sold through a Weaver code (storefront)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sale_ref",
                  "weaver_code",
                  "amount_cents"
                ],
                "properties": {
                  "sale_ref": {
                    "type": "string"
                  },
                  "weaver_code": {
                    "type": "string"
                  },
                  "amount_cents": {
                    "type": "integer"
                  },
                  "app": {
                    "type": "string"
                  },
                  "currency": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ok, weaver_id, credited}"
          }
        },
        "description": "Off by default since v0.3.0 (settings.storefront.enabled) \u2014 Indrevo releases are separate from Orbweave."
      }
    },
    "/api/v1/towns": {
      "get": {
        "summary": "Town autocomplete",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true
          },
          {
            "name": "country",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, towns:[{id,name,state,state_name,country,region,lat,lng,status,population,band,metro_of}]}"
          }
        }
      }
    },
    "/api/v1/arachne/overview": {
      "get": {
        "summary": "Network totals and a daily series (partner / Mission Control secrets)",
        "parameters": [
          {
            "name": "days",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, totals, series}"
          }
        }
      }
    },
    "/api/v1/arachne/towns": {
      "get": {
        "summary": "Town rollups: leads, live sponsors, sponsor cents, scans, signups, gates, redemptions",
        "parameters": [
          {
            "name": "country",
            "in": "query"
          },
          {
            "name": "state",
            "in": "query"
          },
          {
            "name": "q",
            "in": "query"
          },
          {
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, towns:[\u2026]}"
          }
        }
      }
    },
    "/api/v1/argus/latest": {
      "get": {
        "summary": "The latest Argus daily report (SOC guardian / Mission Control)",
        "responses": {
          "200": {
            "description": "{ok, report}"
          }
        }
      }
    },
    "/webhooks/{source}": {
      "post": {
        "summary": "Webhook alternative to the calls above",
        "description": "{event, data}: sponsor.paid \u00b7 sponsor.refunded \u00b7 install.signup \u00b7 install.activity_gate \u00b7 redemption.created \u00b7 redemption.confirmed \u00b7 app.sale \u00b7 ping. Stored in the inbox first, then processed.",
        "parameters": [
          {
            "name": "source",
            "in": "path",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, \u2026, inbox_id}"
          }
        }
      }
    },
    "/api/v1/towns/nearest": {
      "get": {
        "summary": "Nearest town to a point \u2014 the town-band authority",
        "description": "The town a venue sits in and its band ('city' | 'small', TOWN_BANDS_SPEC v1.1: under 50,000 residents and outside a metro orbit = small = half price on local packs). No client decides a band; call this at venue sign-up and price from the answer.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true
          },
          {
            "name": "lng",
            "in": "query",
            "required": true
          },
          {
            "name": "radius_m",
            "in": "query",
            "description": "search radius, default and max 150000"
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, town:{id,name,state,country,lat,lng,population,band,metro_of,distance_km}, small_town_pop_limit}"
          },
          "404": {
            "description": "no town within the radius"
          }
        }
      }
    },
    "/api/v1/towns/{id}": {
      "get": {
        "summary": "Town by id",
        "responses": {
          "200": {
            "description": "{ok, town}"
          }
        }
      }
    },
    "/api/v1/app/version": {
      "get": {
        "summary": "Weaver app: latest version + APK (no auth)",
        "security": [],
        "responses": {
          "200": {
            "description": "{ok, latest, min_supported, apk_url, sha256, bytes, built_at, notes}"
          }
        }
      }
    },
    "/api/v1/app/session": {
      "post": {
        "summary": "Weaver app sign-in (Weaver code + Loom password, + totp when enabled)",
        "security": [],
        "responses": {
          "200": {
            "description": "{ok, token, profile} \u2014 then Authorization: Weaver <token> on /api/v1/app/*"
          },
          "401": {
            "description": "{ok:false, totp_required?:true}"
          }
        }
      }
    },
    "/api/v1/app/{resource}": {
      "get": {
        "summary": "Weaver app reads: me \u00b7 summary \u00b7 sponsors \u00b7 players (counts only) \u00b7 silk \u00b7 top5 \u00b7 gifts \u00b7 messages",
        "description": "Authorization: Weaver <token>. Every figure is the signed-in Weaver's own, at 100%.",
        "responses": {
          "200": {
            "description": "{ok, \u2026}"
          }
        }
      }
    },
    "/api/v1/app/events": {
      "post": {
        "summary": "Weaver app field log (offline queue; idempotent on client_event_id)",
        "responses": {
          "200": {
            "description": "{accepted[], duplicates[], rejected[]}"
          }
        }
      }
    },
    "/api/v1/app/uploads": {
      "post": {
        "summary": "Weaver app photo proof (jpeg/png/webp \u2264 3 MB, base64)",
        "responses": {
          "200": {
            "description": "{ok, id, client_upload_id, bytes, duplicate}"
          }
        }
      }
    },
    "/api/v1/weavers/{code}/gift": {
      "post": {
        "summary": "Register a gift app for a Weaver (source gift-maker)",
        "responses": {
          "200": {
            "description": "{ok, id} or {ok, duplicate:true}"
          }
        }
      }
    },
    "/api/v1/gifts/due": {
      "get": {
        "summary": "Birthdays + rebuilds due (sources gift-maker, argus, mission-control)",
        "parameters": [
          {
            "name": "days",
            "in": "query"
          },
          {
            "name": "birthday_days",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, birthdays[], rebuilds[]}"
          }
        }
      }
    }
  }
}
