{
  "openapi": "3.0.4",
  "info": {
    "title": "RelayPlus Public API",
    "description": "Send approved WhatsApp templates, read delivery status, and keep contacts in step from your own systems. Authenticate with an API key from Settings → API & webhooks: `Authorization: Bearer rly_live_…`. Guide: https://relayplus.app/docs",
    "version": "v1"
  },
  "paths": {
    "/api/public/v1/contacts": {
      "get": {
        "tags": [
          "PublicContacts"
        ],
        "summary": "List contacts.",
        "description": "Requires the `contacts:read` scope. Filters by exact `phone` (any format, normalized to\nE.164), by `tag` (case-insensitive), and by `search` (matches name, email, or phone\ndigits). Cursor-paginated by id; `limit` defaults to 50 and is capped at 200.",
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicContactList"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "PublicContacts"
        ],
        "summary": "Create or update a contact by phone number.",
        "description": "`201` when the contact was created, `200` when the number already existed and was\nupdated. Absent fields are left unchanged; `tags` and `attributes` only ever grow.\nHonours `Idempotency-Key`. Requires the `contacts:write` scope.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiUpsertContactRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiUpsertContactRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiUpsertContactRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicContactView"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicContactView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/contacts/{id}": {
      "get": {
        "tags": [
          "PublicContacts"
        ],
        "summary": "Read a contact.",
        "description": "Requires the `contacts:read` scope. A contact in another workspace is a 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicContactView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/contacts/{id}/tags": {
      "post": {
        "tags": [
          "PublicContacts"
        ],
        "summary": "Add tags to a contact.",
        "description": "Union, case-insensitive — a tag the contact already carries is a no-op. Raises\n`contact.tagged` (webhook + MCP) once per tag genuinely added. Requires the\n`contacts:write` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAddTagsRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAddTagsRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAddTagsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicContactView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/contacts/{id}/tags/{tag}": {
      "delete": {
        "tags": [
          "PublicContacts"
        ],
        "summary": "Remove a tag from a contact.",
        "description": "Case-insensitive and idempotent — removing a tag the contact never had is a no-op. Requires the `contacts:write` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicContactView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/conversations": {
      "get": {
        "tags": [
          "PublicConversations"
        ],
        "summary": "List conversations.",
        "description": "Requires the `conversations:read` scope. Filters by `contactId`, by `status` (`open`,\n`pending`, `snoozed`, `solved` or `expired` — window-derived, not the stored value), and\nby `assigneeId` (a user id, or `unassigned`). Cursor-paginated by id; `limit` defaults to\n50 and is capped at 200. A merged thread is never listed.",
        "parameters": [
          {
            "name": "contactId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "assigneeId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicConversationList"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/conversations/{id}": {
      "get": {
        "tags": [
          "PublicConversations"
        ],
        "summary": "Read a conversation.",
        "description": "Requires the `conversations:read` scope. A conversation in another workspace, or one\nmerged into another thread, is a 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicConversationView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/conversations/{id}/assign": {
      "post": {
        "tags": [
          "PublicConversations"
        ],
        "summary": "Assign or unassign a conversation.",
        "description": "Requires the `conversations:write` scope. `assigneeId` must name a member of this\nworkspace, or be absent/null to unassign. Acting as the key: the assignee is\nnotified the way an inbox assignment notifies them. Honours `Idempotency-Key`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAssignConversationRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAssignConversationRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAssignConversationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicConversationView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/conversations/{id}/status": {
      "post": {
        "tags": [
          "PublicConversations"
        ],
        "summary": "Set a conversation's status.",
        "description": "Requires the `conversations:write` scope. One of `open`, `pending`, `snoozed` or\n`solved` — `expired` is derived, never set. Setting the status already stored is a\nno-op `200`. Honours `Idempotency-Key`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSetConversationStatusRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSetConversationStatusRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSetConversationStatusRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicConversationView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/conversations/{id}/notes": {
      "post": {
        "tags": [
          "PublicConversations"
        ],
        "summary": "Add an internal note to a conversation.",
        "description": "Requires the `conversations:write` scope. The note's author is the key\n(`API · <key name>`). `@mention` a workspace member's name to notify them.\nHonours `Idempotency-Key`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAddNoteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAddNoteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiAddNoteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicNoteView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/messages/template": {
      "post": {
        "tags": [
          "PublicMessages"
        ],
        "summary": "Send an approved template to a phone number.",
        "description": "Creates the contact and the conversation if they do not exist. Answers `202` with\nthe message in status `queued`; `sent`, `delivered`, `read` and `failed` follow, on\n`GET /messages/{id}` and on the `message.*` webhooks. Marketing templates are refused\nfor a contact who opted out. Honours `Idempotency-Key`. Requires `messages:write`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSendTemplateRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSendTemplateRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSendTemplateRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicMessageView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/messages/text": {
      "post": {
        "tags": [
          "PublicMessages"
        ],
        "summary": "Send a free-form text into a conversation whose 24-hour window is open (US-14.01.04.a).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSendTextRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSendTextRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiSendTextRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicMessageView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/messages/{id}": {
      "get": {
        "tags": [
          "PublicMessages"
        ],
        "summary": "Read a message and its current delivery status.",
        "description": "Requires `messages:read`. A message in another workspace is a 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicMessageView"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/templates": {
      "get": {
        "tags": [
          "PublicTemplates"
        ],
        "summary": "List approved templates.",
        "description": "One item per approved language variant. Each item describes the parameters it\ntakes — the header mode, how many body values, and which buttons want a value — so\nyou can build a `POST /messages/template` request without guessing. Cursor-paginated:\npass `nextCursor` back as `cursor` until it is null. Requires the `templates:read` scope.",
        "parameters": [
          {
            "name": "language",
            "in": "query",
            "description": "Filter by Meta language code, e.g. `en_US` or `ar`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by `marketing`, `utility` or `authentication`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` from a previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200. Default 50.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiPublicTemplateList"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PublicApiAddNoteRequest": {
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "description": "The note text. Required.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiAddTagsRequest": {
        "type": "object",
        "properties": {
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to add to the contact. Required.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiApiError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Every error the public API returns (AC-14.01.02.b). Branch on PublicApi.Application.Contracts.ApiError.Code; the\nmessage is for humans and may change. PublicApi.Application.Contracts.ApiError.Fields names the offending part of\na request when there is one — `header`, `body`, `buttons[1]`, `scope` — and is absent\notherwise."
      },
      "PublicApiAssignConversationRequest": {
        "type": "object",
        "properties": {
          "assigneeId": {
            "type": "string",
            "description": "The user to assign the conversation to, or null to unassign.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiMoneyDto": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An amount in minor units — cents, fils — and its ISO 4217 currency. Never a float."
      },
      "PublicApiPublicButtonValue": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "format": "int32"
          },
          "value": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One value for a dynamic button, by its 0-based index in the template."
      },
      "PublicApiPublicContactHint": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Used only if the contact is created by this send.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicContactList": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiPublicContactView"
            },
            "nullable": true
          },
          "nextCursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicContactView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "marketingConsent": {
            "type": "string",
            "description": "`granted`, `revoked` or `unknown`.",
            "nullable": true
          },
          "lifecycleStage": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "description": "How the contact was created: `api`, `inbound`, `manual`, `import`.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "attributes": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "nullable": true
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicConversationList": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiPublicConversationView"
            },
            "nullable": true
          },
          "nextCursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicConversationView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "contactId": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "channelType": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "assigneeId": {
            "type": "string",
            "nullable": true
          },
          "windowExpiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "isWindowOpen": {
            "type": "boolean"
          },
          "lastMessageAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "unreadCount": {
            "type": "integer",
            "format": "int32"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A conversation as an integrator sees it. `Status` is `open`, `pending`, `snoozed`, `solved`\nor `expired` — the window-derived status, the same as the inbox. `IsWindowOpen` says\nwhether a free-form reply is possible right now, without a template."
      },
      "PublicApiPublicMessageView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "conversationId": {
            "type": "string",
            "nullable": true
          },
          "contactId": {
            "type": "string",
            "nullable": true
          },
          "to": {
            "type": "string",
            "nullable": true
          },
          "direction": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "`queued`, `sent`, `delivered`, `read` or `failed`. `sent` means Meta said so.",
            "nullable": true
          },
          "contentType": {
            "type": "string",
            "nullable": true
          },
          "template": {
            "$ref": "#/components/schemas/PublicApiTemplateRefDto"
          },
          "externalId": {
            "type": "string",
            "description": "Meta's own message id (`wamid.…`), once Meta accepted it.",
            "nullable": true
          },
          "failureCode": {
            "type": "string",
            "nullable": true
          },
          "failureMessage": {
            "type": "string",
            "nullable": true
          },
          "cost": {
            "$ref": "#/components/schemas/PublicApiMoneyDto"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicNoteView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "conversationId": {
            "type": "string",
            "nullable": true
          },
          "body": {
            "type": "string",
            "nullable": true
          },
          "authorName": {
            "type": "string",
            "nullable": true
          },
          "mentionedUserIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicTemplateBody": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "nullable": true
          },
          "parameterCount": {
            "type": "integer",
            "description": "How many values `parameters.body` needs, in order.",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicTemplateButton": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "format": "int32"
          },
          "type": {
            "type": "string",
            "description": "`quick_reply`, `url`, `phone_number` or `copy_code`.",
            "nullable": true
          },
          "text": {
            "type": "string",
            "nullable": true
          },
          "parameter": {
            "type": "string",
            "description": "What to send for this index in `parameters.buttons`: `url_suffix`, `coupon_code`, or null when the button takes nothing.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicTemplateHeader": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "description": "`none`, `text`, `image`, `video` or `document`.",
            "nullable": true
          },
          "parameter": {
            "type": "string",
            "description": "What to send in `parameters.header`: `text`, `media_url`, or null when the header takes nothing.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicTemplateList": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiPublicTemplateView"
            },
            "nullable": true
          },
          "nextCursor": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicTemplateParameters": {
        "type": "object",
        "properties": {
          "header": {
            "type": "string",
            "description": "Text, or a public https URL for an image/video/document header — as `GET /templates` says.",
            "nullable": true
          },
          "body": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "One value per `{{n}}` placeholder, in order.",
            "nullable": true
          },
          "buttons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiPublicButtonValue"
            },
            "description": "Only for buttons whose `parameter` is not null.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiPublicTemplateView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "header": {
            "$ref": "#/components/schemas/PublicApiPublicTemplateHeader"
          },
          "body": {
            "$ref": "#/components/schemas/PublicApiPublicTemplateBody"
          },
          "footer": {
            "type": "string",
            "nullable": true
          },
          "buttons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiPublicTemplateButton"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One approved language variant of a template, described so a caller knows exactly what to send."
      },
      "PublicApiSendTemplateRequest": {
        "type": "object",
        "properties": {
          "to": {
            "type": "string",
            "description": "E.164, e.g. `+966501234567`. Required.",
            "nullable": true
          },
          "template": {
            "$ref": "#/components/schemas/PublicApiTemplateRefDto"
          },
          "parameters": {
            "$ref": "#/components/schemas/PublicApiPublicTemplateParameters"
          },
          "channelId": {
            "type": "string",
            "description": "Which WhatsApp number to send from. Required only when the workspace has more than one.",
            "nullable": true
          },
          "contact": {
            "$ref": "#/components/schemas/PublicApiPublicContactHint"
          }
        },
        "additionalProperties": false,
        "description": "Send an approved template to a phone number."
      },
      "PublicApiSendTextRequest": {
        "type": "object",
        "properties": {
          "conversationId": {
            "type": "string",
            "description": "The conversation to reply into. Either this or To is required.",
            "nullable": true
          },
          "to": {
            "type": "string",
            "description": "E.164, e.g. `+966501234567`. Either this or ConversationId is required.",
            "nullable": true
          },
          "body": {
            "type": "string",
            "description": "The message text. Required.",
            "nullable": true
          },
          "channelId": {
            "type": "string",
            "description": "Which WhatsApp number to send from. Required only when the workspace has more than one and To is used.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Send a free-form text reply, either into an existing conversation or to a phone number."
      },
      "PublicApiSetConversationStatusRequest": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "`open`, `pending`, `snoozed` or `solved`. Required.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiTemplateRefDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A template's name and the language variant Meta approved."
      },
      "PublicApiUpsertContactRequest": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string",
            "description": "E.164 with country code, e.g. `+966501234567`. Required.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Overwrites when present. An empty string falls back to the number.",
            "nullable": true
          },
          "language": {
            "type": "string",
            "description": "`ar`, `en`, or `\"\"` to unset. Absent leaves it unchanged.",
            "nullable": true
          },
          "marketingConsent": {
            "type": "string",
            "description": "`granted` or `revoked` — written to the consent ledger with source `API`. `unknown` cannot be set.",
            "nullable": true
          },
          "consentEvidence": {
            "type": "string",
            "description": "What backs the consent claim: an order number, a form id. Defaults to a note naming your key.",
            "nullable": true
          },
          "attributes": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "Merged by key. Never removes an attribute.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Added to the contact. Never removes a tag.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Create a contact by phone number, or update the one that already has it."
      }
    },
    "securitySchemes": {
      "Bearer": {
        "type": "http",
        "description": "A user JWT for /api/v1 and /api/platform/v1; an API key (rly_live_…) for /api/public/v1.",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "security": [
    {
      "Bearer": [ ]
    }
  ],
  "tags": [
    {
      "name": "PublicContacts",
      "description": "Contacts — the people you message, keyed by phone number."
    },
    {
      "name": "PublicConversations",
      "description": "Conversations — the threads messages belong to."
    },
    {
      "name": "PublicMessages",
      "description": "Messages — sending an approved template, and reading what happened to it."
    },
    {
      "name": "PublicTemplates",
      "description": "Approved templates and what each one needs you to send."
    }
  ]
}