{
  "schema": "snappy-card/1",
  "skill": "snappy-whatsapp",
  "tier": "public",
  "origin_tier": "base",
  "purpose": "You handle Snappy's WhatsApp channel: informal client comms, check-ins, meeting reminders, invoice follow-ups, onboarding welcomes, media sends, and Robert self-notifications. Direct WhatsApp Cloud API (Meta Business API) -- no Xano middleware.",
  "verbs": [
    {
      "name": "mark-read",
      "args": [
        "message-id"
      ],
      "effect": "write",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts mark-read <message-id>"
    },
    {
      "name": "media",
      "args": [
        "to",
        "url",
        "caption?"
      ],
      "effect": "send",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts media <to> <url>"
    },
    {
      "name": "notify",
      "args": [
        "text"
      ],
      "effect": "send",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts notify \"<text>\""
    },
    {
      "name": "read",
      "args": [
        "limit?"
      ],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts read"
    },
    {
      "name": "list",
      "args": [
        "limit?"
      ],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts list"
    },
    {
      "name": "thread",
      "args": [
        "from",
        "limit?"
      ],
      "effect": "read",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts thread <from>"
    },
    {
      "name": "send",
      "args": [
        "to",
        "message"
      ],
      "effect": "send",
      "firstCall": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts send <to> \"<message>\""
    }
  ],
  "faces": [],
  "agents": [],
  "uses": [
    "snappy-clients",
    "snappy-calendar",
    "snappy-freshbooks",
    "snappy-ops",
    "snappy-slack"
  ],
  "used_by": [
    "snappy-imessage",
    "snappy-slack",
    "snappy-telegram"
  ],
  "checks": {
    "pass": 52,
    "fail": 2,
    "defer": 7,
    "line": "52 of 54 checks pass",
    "failing": [
      {
        "id": 4,
        "rule": "Near-neighbour descriptions cross-reference each other",
        "says": "near neighbours not named: snappy-runner"
      },
      {
        "id": 15,
        "rule": "Near-miss keys refuse with the correct contract key",
        "says": "0 refusal(s) omit the correcting key; 1 uncovered; 4 case(s) left unfuzzed because the verb has an outside effect"
      }
    ]
  },
  "coverage": null,
  "spec": null,
  "evals": {
    "scenarios": 5,
    "holdout": 2,
    "line": "5 eval scenarios, 2 held out"
  },
  "files": [
    "AGENTS.md",
    "SKILL.md",
    "adapter.ts",
    "api.ts",
    "evals.json",
    "face.test.ts",
    "faces/components/whatsapp-chat.tsx",
    "faces/family.tsx",
    "faces/fixtures/whatsapp-compose.json",
    "faces/fixtures/whatsapp-decision.json",
    "faces/fixtures/whatsapp-list.json",
    "faces/fixtures/whatsapp-message.json",
    "faces/fixtures/whatsapp-thread.json",
    "templates.md",
    "workflows.md"
  ],
  "total_size": 109471,
  "checksum": "sha256:6036622f1700e6017c52660f2a502a7c803c8ef16fd1e7142a8000a6013c0f6e",
  "built_at": "2026-09-09T22:11:30.018Z",
  "openapi": {
    "openapi": "3.1.0",
    "info": {
      "title": "snappy-whatsapp",
      "version": "0.0.0",
      "description": "Sends WhatsApp text and media through Meta's Cloud API from the business number, and reads inbound messages out of the local webhook log as a chat rail or one conversation. Every send stages for the owner's decision except a notify to his own number. Unlike snappy-imessage (local chat.db) and snappy-telegram (Bot API) it needs WHATSAPP_TOKEN and WHATSAPP_PHONE_ID, and inbound arrives only by webhook. Use when asked to message a client on WhatsApp or to read what arrived. Triggers on: whatsapp, wa, send whatsapp, whatsapp client, whatsapp media, cloud api."
    },
    "x-snappy-transport": "cli",
    "servers": [
      {
        "url": "npx tsx ~/.claude/skills/snappy-whatsapp/api.ts",
        "description": "The hand itself. Every path is a verb word, every query or body key an argument."
      }
    ],
    "paths": {
      "/mark-read": {
        "post": {
          "operationId": "snappy_whatsapp_mark_read",
          "summary": "snappy-whatsapp mark-read",
          "x-snappy-effect": "write",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": false,
            "idempotentHint": true,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message-id": {
                      "type": "string",
                      "description": "The Cloud API message id from a webhook payload, for example wamid.HBg...",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "message-id"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/media": {
        "post": {
          "operationId": "snappy_whatsapp_media",
          "summary": "snappy-whatsapp media",
          "x-snappy-effect": "send",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": true,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "to": {
                      "type": "string",
                      "description": "Who receives it, in E.164, for example +14155551212",
                      "x-snappy-position": 0
                    },
                    "url": {
                      "type": "string",
                      "description": "A publicly reachable https link to the image; Meta fetches it, this hand does not upload",
                      "x-snappy-position": 1
                    },
                    "caption": {
                      "type": "string",
                      "description": "Optional words under the image",
                      "x-snappy-position": 2
                    }
                  },
                  "required": [
                    "to",
                    "url"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/notify": {
        "post": {
          "operationId": "snappy_whatsapp_notify",
          "summary": "snappy-whatsapp notify",
          "x-snappy-effect": "send",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": true,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "description": "What to tell the owner on his own number, sent as written",
                      "x-snappy-position": 0
                    }
                  },
                  "required": [
                    "text"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/read": {
        "get": {
          "operationId": "snappy_whatsapp_read",
          "summary": "snappy-whatsapp read",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": false
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many inbound messages to read out of the webhook log, newest last",
                "default": 20,
                "maximum": 200,
                "x-snappy-position": 0
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/list": {
        "get": {
          "operationId": "snappy_whatsapp_list",
          "summary": "snappy-whatsapp list",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": false
          },
          "parameters": [
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many inbound messages to fold into the chat rail, newest last",
                "default": 20,
                "maximum": 200,
                "x-snappy-position": 0
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/thread": {
        "get": {
          "operationId": "snappy_whatsapp_thread",
          "summary": "snappy-whatsapp thread",
          "x-snappy-effect": "read",
          "x-snappy-annotations": {
            "readOnlyHint": true,
            "destructiveHint": false,
            "idempotentHint": false,
            "openWorldHint": false
          },
          "parameters": [
            {
              "name": "from",
              "in": "query",
              "required": true,
              "schema": {
                "type": "string",
                "description": "Whose conversation to read, as the webhook spells the sender: digits with no plus, for example 15555550123",
                "x-snappy-position": 0
              }
            },
            {
              "name": "limit",
              "in": "query",
              "required": false,
              "schema": {
                "type": "integer",
                "description": "How many inbound messages to scan before filtering to that sender",
                "default": 20,
                "maximum": 200,
                "x-snappy-position": 1
              }
            },
            {
              "name": "json",
              "in": "query",
              "required": false,
              "schema": {
                "x-snappy-flag": "--json"
              }
            }
          ],
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      },
      "/send": {
        "post": {
          "operationId": "snappy_whatsapp_send",
          "summary": "snappy-whatsapp send",
          "x-snappy-effect": "send",
          "x-snappy-annotations": {
            "readOnlyHint": false,
            "destructiveHint": true,
            "idempotentHint": false,
            "openWorldHint": true
          },
          "requestBody": {
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "to": {
                      "type": "string",
                      "description": "Who receives it, in E.164, for example +14155551212",
                      "x-snappy-position": 0
                    },
                    "message": {
                      "type": "string",
                      "description": "The message body, sent exactly as written",
                      "x-snappy-position": 1
                    },
                    "json": {
                      "x-snappy-flag": "--json"
                    }
                  },
                  "required": [
                    "to",
                    "message"
                  ]
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "The verb's own answer, as printed by `--json`."
            }
          }
        }
      }
    },
    "components": {
      "schemas": {
        "SnappyRefusal": {
          "type": "object",
          "required": [
            "outcome",
            "code",
            "message"
          ],
          "properties": {
            "outcome": {
              "type": "string",
              "const": "refused"
            },
            "code": {
              "type": "string",
              "description": "A row of the closed refusal table."
            },
            "message": {
              "type": "string"
            },
            "fix": {
              "type": "string"
            }
          }
        }
      }
    },
    "x-snappy-requires": [
      "ROBERT_PHONE",
      "WHATSAPP_PHONE_ID",
      "WHATSAPP_TOKEN"
    ]
  }
}