{"openapi":"3.1.0","info":{"title":"Patch AMD API","version":"1.0.0","description":"Answering machine detection for outbound calls. Stream the callee's audio to `wss://…/v1/stream` and get one decision: human, voicemail, ivr, unavailable, silence or not_sure. These REST endpoints mint signed stream URLs, read detections and report usage. The WebSocket protocol is described in the AsyncAPI document at /asyncapi.json.","contact":{"name":"Manu Labs","email":"leko@manulabs.xyz","url":"https://patchamd.com"},"termsOfService":"https://patchamd.com/terms"},"servers":[{"url":"https://api.patchamd.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"pamd_live_…","description":"An API key from the dashboard. `X-API-Key` is also accepted."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"}}},"Result":{"type":"string","enum":["human","voicemail","ivr","unavailable","silence","not_sure"],"description":"Who picked up. `human`: a live person. `voicemail`, `ivr` (phone menu, hold queue, call screening), `unavailable` (number not in service, mailbox full): a recording. `silence`: nobody spoke after the answer. `not_sure`: the detector could not tell or failed; treat as a person."},"Reason":{"type":"string","enum":["person_waiting","recording_detected","inconclusive","no_speech","time_limit","detector_failed"],"description":"Why. `person_waiting`: a person spoke and is waiting for a reply. `recording_detected`: what was said could only come from a recording or automated system. `inconclusive`: speech that could not be placed either way (result `not_sure`). `no_speech`: nobody spoke. `time_limit`: the listening window ended before the speaker stopped. `detector_failed`: something broke on our side (never billed)."},"CreateStream":{"type":"object","properties":{"webhook_url":{"type":"string","format":"uri","description":"HTTPS URL to receive the `amd.decided` webhook."},"metadata":{"type":"object","additionalProperties":{"type":"string","maxLength":256},"maxProperties":10,"description":"Your identifiers (e.g. call ID), echoed in the webhook. Do not put call content here."},"encoding":{"type":"string","enum":["mulaw","alaw","linear16"],"description":"For raw binary streams only: the audio codec. Omit for Telnyx and Twilio streams, which announce their own."},"sample_rate":{"type":"integer","enum":[8000,16000,24000,44100,48000],"default":8000,"description":"For raw streams only."},"events":{"type":"boolean","default":false,"description":"Also send decision events on a Telnyx or Twilio socket (raw streams always get them)."}}},"Stream":{"type":"object","required":["id","url","token","expires_at"],"properties":{"id":{"type":"string","description":"The detection ID.","example":"det_4f9Qm2LxV7cT1bZr8KpA"},"url":{"type":"string","description":"The stream URL with the token in its query string. Give it to Telnyx as `stream_url`, or connect to it.","example":"wss://api.patchamd.com/v1/stream?token=det_…"},"base_url":{"type":"string","description":"The stream URL without a query string, for Twilio `<Stream url>`."},"token":{"type":"string","description":"The signed token on its own, for Twilio `<Parameter name=\"token\">`. Single use."},"expires_at":{"type":"string","format":"date-time","description":"The stream must open before this (10 minutes)."}}},"Detection":{"type":"object","required":["id","status","billable","created_at"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","streaming","decided","ended"],"description":"`ended`: the stream closed (e.g. hang-up) before a decision."},"carrier":{"type":["string","null"],"enum":["telnyx","twilio","raw",null]},"result":{"oneOf":[{"$ref":"#/components/schemas/Result"},{"type":"null"}]},"reason":{"oneOf":[{"$ref":"#/components/schemas/Reason"},{"type":"null"}]},"at_ms":{"type":["integer","null"]},"billable":{"type":"boolean"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"created_at":{"type":"string","format":"date-time"},"decided_at":{"type":["string","null"],"format":"date-time"}}},"Usage":{"type":"object","required":["from","to","total","days"],"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"total":{"type":"integer","description":"Billable detections in the range."},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"key_id":{"type":"string"},"detections":{"type":"integer"}}}}}}}},"paths":{"/v1/streams":{"post":{"operationId":"createStream","summary":"Mint a signed stream URL","description":"For dialers that cannot send headers on the media WebSocket. Returns a single-use URL that expires in 10 minutes. Start it when the call is answered (Twilio `<Start><Stream>` in the answer TwiML; Telnyx `streaming_start` on `call.answered`, or `stream_url` on dial).","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStream"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stream"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key"},"429":{"description":"Rate limit exceeded"}}}},"/v1/detections/{id}":{"get":{"operationId":"getDetection","summary":"Read a detection","description":"The outcome of one stream. The words heard are never stored, so `heard` is only in the decision event and webhook.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Detection"}}}},"404":{"description":"Not found"}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Billable detections per day","description":"Counts detections where the call was answered and the detector decided. Detector failures are not counted. UTC days.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"Inclusive. Default: the first of this month."},{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Exclusive. Default: tomorrow."},{"name":"key","in":"query","schema":{"type":"string","enum":["all","this"],"default":"all"},"description":"`this`: only the calling key."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"}}}}}}}},"webhooks":{"amd.decided":{"post":{"summary":"A detection decided","description":"Sent to the stream's `webhook_url`. Signed per Standard Webhooks: verify `webhook-signature` (`v1,<base64 HMAC-SHA256>` of `{webhook-id}.{webhook-timestamp}.{body}`, keyed by the base64-decoded part of your `whsec_…` secret from the dashboard). Retried at 2 s and 10 s on failure.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["type","timestamp","data"],"properties":{"type":{"const":"amd.decided"},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","required":["detection_id","result","reason","at_ms","heard"],"properties":{"detection_id":{"type":"string","example":"det_4f9Qm2LxV7cT1bZr8KpA"},"result":{"type":"string","enum":["human","voicemail","ivr","unavailable","silence","not_sure"],"description":"Who picked up. `human`: a live person. `voicemail`, `ivr` (phone menu, hold queue, call screening), `unavailable` (number not in service, mailbox full): a recording. `silence`: nobody spoke after the answer. `not_sure`: the detector could not tell or failed; treat as a person."},"reason":{"type":"string","enum":["person_waiting","recording_detected","inconclusive","no_speech","time_limit","detector_failed"],"description":"Why. `person_waiting`: a person spoke and is waiting for a reply. `recording_detected`: what was said could only come from a recording or automated system. `inconclusive`: speech that could not be placed either way (result `not_sure`). `no_speech`: nobody spoke. `time_limit`: the listening window ended before the speaker stopped. `detector_failed`: something broke on our side (never billed)."},"at_ms":{"type":"integer","description":"Milliseconds from the start of the stream (the answer) to the decision.","example":1840},"heard":{"type":"string","description":"The words heard when the decision was made. Sent to you, never stored by us.","example":"Hello?"},"carrier":{"type":"string","enum":["telnyx","twilio","raw"]},"call_id":{"type":["string","null"],"description":"Telnyx call_control_id or Twilio CallSid, from the stream's start message."},"stream_id":{"type":["string","null"]},"metadata":{"type":"object","additionalProperties":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Any 2xx acknowledges it."}}}}}}