{
  "openapi": "3.0.3",
  "info": {
    "title": "App Connect Plugin Server API",
    "version": "1.7.38",
    "description": "Provider-side HTTP contract for independently hosted App Connect plugin servers. Most paths mirror the JavaScript plugin template; /oauth/callback is an illustrative plugin-owned target selected through OAuth state.redirectTo. Complete URLs in the plugin manifest, not these sample paths or servers, are authoritative.\n\nApp Connect calls registration, token, invocation, and license endpoints server-to-server. Its browser extension calls OAuth endpoints directly, so those endpoints need restricted CORS and a plugin-owned correlation or session mechanism. Production endpoints must use HTTPS. Requests can contain RingCentral access tokens, plugin bearer credentials, user configuration, contact and communications data, and recording URLs containing access tokens. Do not log credentials, and retain personal data only as required.\n\nThe current protocol has no uninstall callback. Removing a plugin deletes the App Connect registration only, so plugin providers need their own stale-account cleanup, credential revocation, and data-retention policy.",
    "contact": {
      "name": "RingCentral Labs",
      "url": "https://github.com/ringcentral/rc-unified-crm-extension"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/license/mit"
    }
  },
  "externalDocs": {
    "description": "App Connect plugin developer guide",
    "url": "https://appconnect.labs.ringcentral.com/developers/plugins/"
  },
  "servers": [
    {
      "url": "https://{pluginHost}",
      "description": "Developer-hosted plugin server",
      "variables": {
        "pluginHost": {
          "default": "plugins.example.com",
          "description": "Hostname of the deployed plugin server. Manifest endpoint URLs remain authoritative."
        }
      }
    },
    {
      "url": "http://localhost:6066",
      "description": "Local plugin development server"
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Optional process-liveness convention from the JavaScript template; it is not part of App Connect invocation readiness."
    },
    {
      "name": "Registration",
      "description": "Server-to-server RingCentral account validation and plugin-issued credential bootstrap."
    },
    {
      "name": "Token lifecycle",
      "description": "Server-to-server validation and rotation of the stored account-scoped plugin credential."
    },
    {
      "name": "Invocation",
      "description": "Synchronous payload transformation, asynchronous background work, and App Connect completion callbacks."
    },
    {
      "name": "Licensing",
      "description": "Optional advisory account-entitlement status displayed by the App Connect browser client."
    },
    {
      "name": "OAuth",
      "description": "Optional browser-facing authorization URL, state, logout, and plugin-owned provider callback endpoints."
    }
  ],
  "security": [],
  "paths": {
    "/isAlive": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Check plugin-server liveness",
        "description": "Optional deployment liveness convention implemented by the JavaScript template. App Connect does not call it during registration or invocation. The template returns the static text OK; it does not verify databases, third-party providers, or other readiness dependencies.",
        "operationId": "getPluginServerIsAlive",
        "security": [],
        "x-app-connect-required": false,
        "responses": {
          "200": {
            "description": "The route returned the literal text OK. This confirms only process liveness, not dependency readiness.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": "OK"
              }
            }
          }
        }
      }
    },
    "/admin/register": {
      "post": {
        "tags": [
          "Registration"
        ],
        "summary": "Register a RingCentral account",
        "description": "Required reference operation for manifest userRegisterEndpointUrl. After App Connect validates the installing administrator, it sends the RingCentral access token and rcAccountId to the configured URL; pluginId is not included in the JSON body. The plugin server must independently validate the token with RingCentral, confirm that its account matches rcAccountId, and issue a non-empty account-scoped bearer credential in jwtToken. A host serving multiple plugins must derive plugin identity from the configured URL or trusted server configuration. The unmodified template reads req.params.pluginId even though its route has no :pluginId parameter, so adapt it before relying on plugin-id claims. App Connect treats jwtToken as opaque and stores it for protected provider calls. Never log or retain the RingCentral access token.",
        "operationId": "registerPluginAccount",
        "security": [],
        "x-app-connect-endpoint-source": "userRegisterEndpointUrl",
        "requestBody": {
          "$ref": "#/components/requestBodies/PluginRegistration"
        },
        "responses": {
          "200": {
            "description": "The RingCentral account was validated and a non-empty plugin bearer credential was issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginRegistrationResponse"
                },
                "examples": {
                  "registered": {
                    "summary": "Plugin credential issued",
                    "value": {
                      "jwtToken": "eyJhbGciOi...plugin-account-token"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The JSON body was invalid, the RingCentral access token was missing or rejected, or RingCentral identity lookup failed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TextError"
                },
                "example": "Plugin registration failed"
              }
            }
          },
          "403": {
            "description": "The validated RingCentral identity does not belong to rcAccountId.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TextError"
                },
                "example": "rcAccountId mismatch"
              }
            }
          }
        }
      }
    },
    "/token/sync": {
      "post": {
        "tags": [
          "Token lifecycle"
        ],
        "summary": "Validate or rotate the plugin credential",
        "description": "Reference path for manifest tokenSyncUrl, required when isAsync is true. Before each asynchronous call, SMS, or fax dispatch, App Connect POSTs an empty JSON object with the stored plugin bearer. The response body is ignored. Return a replacement credential in X-Refreshed-JWT-Token when rotation is required; App Connect uses it for the immediately following invocation and persists it after dispatch. Refresh headers from the asynchronous invocation are ignored. A callback-enabled call token-sync failure marks the task failed without rolling back the already-saved CRM activity; SMS and fax logging continues after a failed fire-and-forget dispatch.",
        "operationId": "syncPluginToken",
        "security": [
          {
            "pluginBearer": []
          }
        ],
        "x-app-connect-endpoint-source": "tokenSyncUrl",
        "requestBody": {
          "$ref": "#/components/requestBodies/EmptyJsonObject"
        },
        "responses": {
          "200": {
            "description": "The credential was accepted. The body is ignored; the template returns OK. An optional X-Refreshed-JWT-Token rotates the credential.",
            "headers": {
              "X-Refreshed-JWT-Token": {
                "$ref": "#/components/headers/RefreshedPluginJwt"
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": "OK"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/PluginBearerUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PluginBearerForbidden"
          },
          "500": {
            "description": "The plugin server could not validate or rotate the credential.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TextError"
                },
                "example": "Token sync failed"
              }
            }
          }
        }
      }
    },
    "/plugin/sync": {
      "post": {
        "tags": [
          "Invocation"
        ],
        "summary": "Process a logging payload synchronously",
        "description": "Reference path for manifest endpointUrl when isAsync is false or omitted. App Connect invokes installed synchronous plugins sequentially before the connector creates or updates the CRM activity. The request envelope contains data, optional config, and, for call flows, hashedExtensionId. The response must be the bare logging payload, not the request envelope; it becomes input to the next plugin and then the connector. Preserve every downstream-required field. A network error, non-2xx response, or unusable response fails primary logging. App Connect persists X-Refreshed-JWT-Token from a successful synchronous response.",
        "operationId": "invokeSynchronousPlugin",
        "security": [
          {
            "pluginBearer": []
          }
        ],
        "x-app-connect-endpoint-source": "endpointUrl",
        "requestBody": {
          "$ref": "#/components/requestBodies/PluginInvocation"
        },
        "responses": {
          "200": {
            "description": "The transformed logging payload itself. App Connect passes it to the next synchronous plugin or connector; do not wrap it in a data envelope.",
            "headers": {
              "X-Refreshed-JWT-Token": {
                "$ref": "#/components/headers/RefreshedPluginJwt"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoggingPayload"
                },
                "examples": {
                  "transformedCall": {
                    "summary": "Call note enriched by the plugin",
                    "value": {
                      "logInfo": {
                        "sessionId": "session-123",
                        "direction": "Outbound",
                        "startTime": "2026-07-14T09:30:00.000Z",
                        "duration": 120,
                        "result": "Completed",
                        "from": {
                          "phoneNumber": "+14155550100"
                        },
                        "to": {
                          "phoneNumber": "+14155550101"
                        }
                      },
                      "contactId": "contact-123",
                      "contactType": "Contact",
                      "contactName": "Ada Lovelace",
                      "note": "FOLLOW UP TOMORROW.",
                      "additionalSubmission": {}
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/PluginBearerUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PluginBearerForbidden"
          },
          "500": {
            "description": "The plugin failed to process the logging payload.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TextError"
                },
                "example": "Plugin request failed"
              }
            }
          }
        }
      }
    },
    "/plugin/async": {
      "post": {
        "tags": [
          "Invocation"
        ],
        "summary": "Start asynchronous plugin processing",
        "description": "Reference path for manifest endpointUrl when isAsync is true. For call create and update, App Connect saves the CRM activity first, creates a one-week callback task, synchronizes the plugin token, and POSTs data, config, asyncTaskId, callbackUrl, and hashedExtensionId. Any 2xx means dispatch completed; App Connect ignores the body and refresh headers. The plugin later reports completion to callbackUrl, and failure does not roll back the activity. For SMS and fax, App Connect synchronizes the token and starts an unawaited fire-and-forget POST before connector logging continues. That variant sends only data and config, with the original message payload nested at data.logInfo.logInfo. The unmodified JavaScript template rejects this variant because it requires callback fields; adapt it when declaring asynchronous SMS or fax support.",
        "operationId": "invokeAsynchronousPlugin",
        "security": [
          {
            "pluginBearer": []
          }
        ],
        "x-app-connect-endpoint-source": "endpointUrl",
        "requestBody": {
          "$ref": "#/components/requestBodies/AsyncPluginInvocation"
        },
        "callbacks": {
          "asyncPluginCompletion": {
            "$ref": "#/components/callbacks/AsyncPluginCompletion"
          }
        },
        "responses": {
          "200": {
            "description": "Template acknowledgment for a callback-enabled task. For call dispatch, App Connect treats any 2xx as dispatched without inspecting accepted, asyncTaskId, or message; SMS and fax do not await or inspect the response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncAcceptanceResponse"
                },
                "examples": {
                  "accepted": {
                    "summary": "Callback-enabled call task accepted",
                    "value": {
                      "accepted": true,
                      "asyncTaskId": "4d3e22d7-2764-42d5-9d1c-dae7b45e40b3"
                    }
                  },
                  "fireAndForgetAccepted": {
                    "summary": "Fire-and-forget message task accepted",
                    "value": {
                      "accepted": true
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "The provider accepted asynchronous processing. App Connect treats this the same as any other successful 2xx dispatch and ignores the body."
          },
          "204": {
            "description": "The provider accepted asynchronous processing without a response body. App Connect treats this as a successful dispatch."
          },
          "400": {
            "description": "The unmodified template returns this whenever asyncTaskId or callbackUrl is absent, including for the documented fire-and-forget SMS or fax request unless the handler is adapted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncRejectedResponse"
                },
                "example": {
                  "accepted": false,
                  "message": "asyncTaskId and callbackUrl are required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/PluginBearerUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PluginBearerForbidden"
          },
          "500": {
            "description": "The plugin server could not accept the task.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/TextError"
                },
                "example": "Plugin request failed"
              }
            }
          }
        }
      }
    },
    "/license": {
      "get": {
        "tags": [
          "Licensing"
        ],
        "summary": "Get plugin license status",
        "description": "Optional reference path for manifest licenseStatusUrl. App Connect calls it server-to-server with the account-scoped plugin bearer only for a plugin marked requireLicense. licenseStatus drives a browser warning, licenseStatusDescription appears on the configuration page, and optional errorMessage appears in the installed-plugin list. The result is advisory: current App Connect logging code does not gate plugin invocation with it. Missing licenseStatus is normalized to false and unavailable. Credential-refresh headers from this response are not persisted.",
        "operationId": "getPluginServerLicenseStatus",
        "security": [
          {
            "pluginBearer": []
          }
        ],
        "x-app-connect-endpoint-source": "licenseStatusUrl",
        "x-app-connect-required": false,
        "responses": {
          "200": {
            "description": "The account entitlement result forwarded to the browser client. Missing licenseStatus is treated as unavailable and false; additional fields are preserved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginLicenseStatus"
                },
                "examples": {
                  "active": {
                    "summary": "Active entitlement",
                    "value": {
                      "licenseStatus": true,
                      "licenseStatusDescription": "Active"
                    }
                  },
                  "inactive": {
                    "summary": "Subscription required",
                    "value": {
                      "licenseStatus": false,
                      "licenseStatusDescription": "Subscription required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/PluginBearerUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PluginBearerForbidden"
          },
          "500": {
            "description": "The plugin server could not determine license status."
          }
        }
      }
    },
    "/authUrl": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Create a plugin authorization URL",
        "description": "Optional reference path for manifest authorizationUrl. The browser client directly calls authorizationUrl with pluginId as an untrusted routing hint; no plugin bearer credential is intentionally supplied. Shared Axios defaults can nevertheless add ambient RingCentral identity headers and sometimes an App Connect Authorization header; these values are not a supported plugin authentication contract and must not be trusted or logged. Return the provider URL as an object containing authUrl. For compatibility, the client also accepts a bare URL string. The provider URL must redirect to the App Connect extension URI and include state with from=plugin, the plugin callback URL in redirectTo, and plugin-generated anti-CSRF correlation that the callback validates. Restrict CORS to the supported extension origin and required headers.",
        "operationId": "getPluginAuthorizationUrl",
        "security": [],
        "x-app-connect-endpoint-source": "authorizationUrl",
        "x-app-connect-required": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/pluginId"
          }
        ],
        "responses": {
          "200": {
            "description": "Preferred response is a JSON object containing authUrl; the current client also accepts a bare URL string for compatibility.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PluginAuthUrlResponse"
                    },
                    {
                      "type": "string",
                      "format": "uri"
                    }
                  ]
                },
                "example": {
                  "authUrl": "https://provider.example.com/oauth/authorize?client_id=plugin-client&redirect_uri=https%3A%2F%2Fringcentral.github.io%2Fringcentral-embeddable%2Fredirect.html&state=%7B%22from%22%3A%22plugin%22%2C%22redirectTo%22%3A%22https%3A%2F%2Fplugins.example.com%2Foauth%2Fcallback%22%2C%22nonce%22%3A%22csrf-correlation%22%7D"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                },
                "example": "https://provider.example.com/oauth/authorize?client_id=plugin-client"
              }
            }
          },
          "400": {
            "description": "The plugin could not create an authorization URL."
          },
          "500": {
            "description": "The plugin authorization provider was unavailable."
          }
        }
      }
    },
    "/checkAuth": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Check plugin authorization state",
        "description": "Optional reference path for manifest authStateUrl. The browser calls this URL directly without deliberately attaching a plugin credential or operation-specific user parameter. Shared Axios defaults can nevertheless add ambient App Connect headers such as rc-extension-id, rc-account-id, developer-author-name, and sometimes an App Connect Authorization header. Those ambient values are not a supported plugin authentication contract and must not be trusted or logged. A remote plugin needs its own trusted correlation or session mechanism to report per-user state. The client reads successful and can display returnMessage. Restrict CORS appropriately.",
        "operationId": "getPluginAuthorizationState",
        "security": [],
        "x-app-connect-endpoint-source": "authStateUrl",
        "x-app-connect-required": false,
        "responses": {
          "200": {
            "description": "Authorization state for the correlated plugin user. The client uses successful and displays optional returnMessage.message and messageType with a fixed 3000 ms duration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginAuthStateResponse"
                },
                "examples": {
                  "connected": {
                    "summary": "Plugin account connected",
                    "value": {
                      "successful": true
                    }
                  },
                  "disconnected": {
                    "summary": "Plugin account not connected",
                    "value": {
                      "successful": false
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The plugin could not determine authorization state. The client treats the request as disconnected and displays returnMessage when the error response supplies one."
          }
        }
      }
    },
    "/logout": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Disconnect the plugin user",
        "description": "Optional reference path for manifest logoutUrl. The browser directly POSTs an object whose optional jwtToken is its App Connect CRM-session token, not the plugin bearer. The value can be absent, and a separately hosted plugin generally cannot validate it; treat it as opaque and secret and use a trusted plugin-owned correlation or session mechanism. Ambient shared Axios headers are not a supported identity contract. The UI changes to logged out only when the response body contains successful: true. Restrict CORS appropriately.",
        "operationId": "logoutPluginUser",
        "security": [],
        "x-app-connect-endpoint-source": "logoutUrl",
        "x-app-connect-required": false,
        "requestBody": {
          "$ref": "#/components/requestBodies/PluginLogout"
        },
        "responses": {
          "200": {
            "description": "Logout processing completed. The client considers the user disconnected only when the JSON body contains successful: true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginAuthStateResponse"
                },
                "example": {
                  "successful": true
                }
              }
            }
          },
          "400": {
            "description": "The logout request could not be processed."
          },
          "500": {
            "description": "The plugin could not disconnect the user."
          }
        }
      }
    },
    "/oauth/callback": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Complete plugin OAuth authorization",
        "description": "Optional plugin-owned target selected by OAuth state.redirectTo; this path is not fixed and is not implemented by the JavaScript template. After the provider redirects to the App Connect extension URI, the browser calls redirectTo with hashedExtensionId and callbackUri. No plugin bearer is intentionally supplied. Shared Axios defaults can nevertheless add ambient RingCentral identity headers and sometimes an App Connect Authorization header; these values are not a supported plugin authentication contract and must not be trusted or logged. The current client interpolates callbackUri without percent-encoding, so fields after its first ampersand can arrive as top-level query parameters. Accept both a correctly encoded URI and the current flattened form, including OAuth errors. Validate the full state and anti-CSRF correlation before code exchange. hashedExtensionId is pseudonymous correlation, not authentication. Any 2xx is treated as success and the body is ignored.",
        "operationId": "completePluginOAuth",
        "security": [],
        "x-app-connect-endpoint-source": "state.redirectTo",
        "x-app-connect-required": false,
        "parameters": [
          {
            "$ref": "#/components/parameters/hashedExtensionId"
          },
          {
            "$ref": "#/components/parameters/callbackUri"
          },
          {
            "$ref": "#/components/parameters/legacyOAuthCode"
          },
          {
            "$ref": "#/components/parameters/legacyOAuthScope"
          },
          {
            "$ref": "#/components/parameters/legacyOAuthState"
          },
          {
            "$ref": "#/components/parameters/legacyOAuthError"
          },
          {
            "$ref": "#/components/parameters/legacyOAuthErrorDescription"
          }
        ],
        "responses": {
          "200": {
            "description": "The provider authorization code was exchanged and stored. The App Connect browser client uses only the successful HTTP status and ignores the response body."
          },
          "400": {
            "description": "The callback URI, code, or state was invalid."
          },
          "500": {
            "description": "The provider token exchange failed."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "pluginBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Account-scoped credential returned from userRegisterEndpointUrl. Despite the jwtToken property name, App Connect treats it as opaque and never inspects claims. The plugin server owns representation, signature, expiry, revocation, account scope, and plugin scope validation. App Connect stores it server-side and sends it only on protected provider calls."
      }
    },
    "headers": {
      "RefreshedPluginJwt": {
        "description": "Optional replacement for the stored account-scoped plugin credential. App Connect reads and persists this header from token synchronization and synchronous invocation responses. It ignores the header on license and asynchronous invocation responses.",
        "schema": {
          "type": "string",
          "minLength": 1
        }
      }
    },
    "parameters": {
      "pluginId": {
        "name": "pluginId",
        "in": "query",
        "required": true,
        "description": "Developer Console plugin identifier appended by the browser client to authorizationUrl. It is an untrusted routing hint and is not proof of user, account, or plugin-server authorization.",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "example": "plugin.example"
      },
      "hashedExtensionId": {
        "name": "hashedExtensionId",
        "in": "query",
        "required": true,
        "description": "Deployment-scoped pseudonymous extension identifier read from App Connect client storage. It can be empty when user metadata is unavailable. Use it only for correlation after validating OAuth state; it is not an authentication credential.",
        "schema": {
          "type": "string"
        },
        "example": "6b5d3fcae1f7"
      },
      "callbackUri": {
        "name": "callbackUri",
        "in": "query",
        "required": true,
        "description": "URI on the App Connect extension redirect page containing the provider result and OAuth state. Correct clients percent-encode the full value. The current client inserts it raw, so this parameter can contain only the prefix before the first ampersand; reconstruct remaining top-level fields before validating state or exchanging a code.",
        "schema": {
          "type": "string",
          "format": "uri"
        },
        "example": "https://ringcentral.github.io/ringcentral-embeddable/redirect.html?code=provider-code&state=plugin-state"
      },
      "legacyOAuthCode": {
        "name": "code",
        "in": "query",
        "required": false,
        "description": "Provider authorization code that appears as a top-level parameter when it follows the first ampersand in the current client's unencoded callbackUri. Prefer a correctly encoded callbackUri.",
        "schema": {
          "type": "string"
        }
      },
      "legacyOAuthScope": {
        "name": "scope",
        "in": "query",
        "required": false,
        "description": "Provider scope that appears as a top-level parameter when it follows the first ampersand in the current client's unencoded callbackUri. Prefer a correctly encoded callbackUri.",
        "schema": {
          "type": "string"
        }
      },
      "legacyOAuthState": {
        "name": "state",
        "in": "query",
        "required": false,
        "description": "OAuth state that appears as a top-level parameter when it follows the first ampersand in the current client's unencoded callbackUri. The plugin must reconstruct and validate the complete state.",
        "schema": {
          "type": "string"
        }
      },
      "legacyOAuthError": {
        "name": "error",
        "in": "query",
        "required": false,
        "description": "Provider OAuth error flattened from the current client's unencoded callbackUri. When present, do not attempt a code exchange.",
        "schema": {
          "type": "string"
        }
      },
      "legacyOAuthErrorDescription": {
        "name": "error_description",
        "in": "query",
        "required": false,
        "description": "Provider-supplied error detail flattened from callbackUri. Treat it as untrusted display or diagnostic text and do not echo secrets.",
        "schema": {
          "type": "string"
        }
      }
    },
    "callbacks": {
      "AsyncPluginCompletion": {
        "{$request.body#/callbackUrl}": {
          "post": {
            "tags": [
              "Invocation"
            ],
            "summary": "Report asynchronous call-plugin completion",
            "description": "Callback into App Connect for callback-enabled asynchronous call create and update tasks. The random task UUID embedded in callbackUrl is the callback bearer capability; no Authorization header is required. Tasks expire after one week and are deleted after successful completion. With successful true, App Connect reads the current CRM activity, appends note to existing Agent notes separated by a blank line, updates the activity, and deletes the task. With successful false, App Connect marks the task failed, stores message, ignores note, and still returns HTTP 200 with response successful true to acknowledge that the failure report was recorded.",
            "operationId": "postAsyncPluginCompletion",
            "security": [],
            "requestBody": {
              "required": true,
              "content": {
                "application/json": {
                  "schema": {
                    "$ref": "#/components/schemas/AsyncPluginCallbackRequest"
                  },
                  "examples": {
                    "completed": {
                      "summary": "Background work completed",
                      "value": {
                        "successful": true,
                        "message": "Async plugin completed",
                        "note": "Recording uploaded"
                      }
                    },
                    "failed": {
                      "summary": "Background work failed",
                      "value": {
                        "successful": false,
                        "message": "Upload failed",
                        "note": ""
                      }
                    }
                  }
                }
              },
              "description": "Completion report for the task identified by the callback URL. successful describes plugin work, not HTTP transport success."
            },
            "responses": {
              "200": {
                "description": "The callback was accepted. A plugin-reported failure also receives 200 after App Connect marks the task failed.",
                "content": {
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/AsyncPluginCallbackResponse"
                    },
                    "example": {
                      "successful": true
                    }
                  }
                }
              },
              "400": {
                "description": "successful was missing or was not a JSON boolean.",
                "content": {
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/AsyncPluginCallbackResponse"
                    },
                    "example": {
                      "successful": false,
                      "message": "successful is required"
                    }
                  }
                }
              },
              "404": {
                "description": "The callback task was not found or has expired.",
                "content": {
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/AsyncPluginCallbackResponse"
                    },
                    "example": {
                      "successful": false,
                      "message": "Async task not found"
                    }
                  }
                }
              },
              "500": {
                "description": "App Connect could not process a valid success callback, for example because the original call log, CRM session, CRM read, or CRM update was unavailable. The task is marked failed when it was successfully loaded.",
                "content": {
                  "application/json": {
                    "schema": {
                      "$ref": "#/components/schemas/AsyncPluginCallbackResponse"
                    },
                    "example": {
                      "successful": false,
                      "message": "Async plugin callback failed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "requestBodies": {
      "PluginRegistration": {
        "required": true,
        "description": "Secret RingCentral access token plus the claimed account ID for one plugin registration. No plugin ID is included; bind plugin identity from the configured endpoint or trusted server configuration.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PluginRegistrationRequest"
            },
            "examples": {
              "accountRegistration": {
                "summary": "Register an account",
                "value": {
                  "rcAccessToken": "ringcentral-admin-access-token",
                  "rcAccountId": "123456789"
                }
              }
            }
          }
        }
      },
      "EmptyJsonObject": {
        "required": true,
        "description": "App Connect sends an empty JSON object when synchronizing the plugin credential.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/EmptyObject"
            },
            "example": {}
          }
        }
      },
      "PluginInvocation": {
        "required": true,
        "description": "Synchronous invocation envelope. data is the current connector logging payload, config is the user's saved plugin configuration or null, and call flows can include hashedExtensionId. The response is data itself, not this envelope.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PluginInvocationRequest"
            },
            "examples": {
              "call": {
                "summary": "Synchronous call plugin",
                "value": {
                  "data": {
                    "logInfo": {
                      "sessionId": "session-123",
                      "direction": "Outbound",
                      "startTime": "2026-07-14T09:30:00.000Z",
                      "duration": 120,
                      "result": "Completed",
                      "from": {
                        "phoneNumber": "+14155550100"
                      },
                      "to": {
                        "phoneNumber": "+14155550101"
                      }
                    },
                    "contactId": "contact-123",
                    "contactType": "Contact",
                    "contactName": "Ada Lovelace",
                    "note": "Follow up tomorrow.",
                    "additionalSubmission": {}
                  },
                  "config": {
                    "ignoreLetters": {
                      "value": "ac"
                    }
                  },
                  "hashedExtensionId": "6b5d3fcae1f7"
                }
              },
              "sms": {
                "summary": "Synchronous SMS plugin",
                "value": {
                  "data": {
                    "logInfo": {
                      "messages": [
                        {
                          "id": "message-123",
                          "type": "SMS",
                          "subject": "Customer reply",
                          "direction": "Outbound",
                          "creationTime": "2026-07-14T09:30:00.000Z"
                        }
                      ],
                      "correspondents": [
                        {
                          "phoneNumber": "+14155550101"
                        }
                      ],
                      "conversationId": "conversation-123",
                      "conversationLogId": "conversation-log-123"
                    },
                    "contactId": "contact-123",
                    "contactName": "Ada Lovelace",
                    "additionalSubmission": {}
                  },
                  "config": null
                }
              }
            }
          }
        }
      },
      "AsyncPluginInvocation": {
        "required": true,
        "description": "Either a callback-enabled call invocation or the current fire-and-forget SMS or fax invocation. Call requests include task and callback fields. SMS and fax omit them and wrap the original message payload under data.logInfo, producing message details at data.logInfo.logInfo.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/AsyncCallbackInvocationRequest"
                },
                {
                  "$ref": "#/components/schemas/AsyncFireAndForgetInvocationRequest"
                }
              ]
            },
            "examples": {
              "callWithCallback": {
                "summary": "Asynchronous call plugin with completion callback",
                "value": {
                  "data": {
                    "logInfo": {
                      "sessionId": "session-123",
                      "direction": "Outbound",
                      "startTime": "2026-07-14T09:30:00.000Z",
                      "duration": 120,
                      "result": "Completed"
                    },
                    "note": "Follow up tomorrow.",
                    "additionalSubmission": {}
                  },
                  "config": null,
                  "asyncTaskId": "4d3e22d7-2764-42d5-9d1c-dae7b45e40b3",
                  "callbackUrl": "https://app-connect.example.com/plugin/async-callback/4d3e22d7-2764-42d5-9d1c-dae7b45e40b3",
                  "hashedExtensionId": "6b5d3fcae1f7"
                }
              },
              "callUpdateWithCallback": {
                "summary": "Asynchronous call-log update with the update payload nested under data.logInfo",
                "value": {
                  "data": {
                    "logInfo": {
                      "sessionId": "session-123",
                      "note": "Follow up completed.",
                      "recordingLink": "https://media.example.com/recordings/call-123",
                      "result": "Completed"
                    }
                  },
                  "config": null,
                  "asyncTaskId": "4d3e22d7-2764-42d5-9d1c-dae7b45e40b3",
                  "callbackUrl": "https://app-connect.example.com/plugin/async-callback/4d3e22d7-2764-42d5-9d1c-dae7b45e40b3",
                  "hashedExtensionId": "6b5d3fcae1f7"
                }
              },
              "smsFireAndForget": {
                "summary": "Current asynchronous SMS fire-and-forget payload",
                "value": {
                  "data": {
                    "logInfo": {
                      "logInfo": {
                        "messages": [
                          {
                            "id": "message-123",
                            "type": "SMS",
                            "subject": "Customer reply",
                            "direction": "Outbound",
                            "creationTime": "2026-07-14T09:30:00.000Z"
                          }
                        ],
                        "correspondents": [
                          {
                            "phoneNumber": "+14155550101"
                          }
                        ],
                        "conversationId": "conversation-123",
                        "conversationLogId": "conversation-log-123"
                      },
                      "contactId": "contact-123",
                      "contactName": "Ada Lovelace",
                      "additionalSubmission": {}
                    }
                  },
                  "config": null
                }
              }
            }
          }
        }
      },
      "PluginLogout": {
        "required": true,
        "description": "Browser logout body. jwtToken is an optional App Connect CRM-session token, not the plugin bearer credential; it can be omitted and must be treated as secret and opaque.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PluginLogoutRequest"
            },
            "example": {
              "jwtToken": "app-connect-user-session-token"
            }
          }
        }
      }
    },
    "responses": {
      "PluginBearerUnauthorized": {
        "description": "The plugin bearer credential is missing or invalid.",
        "content": {
          "text/plain": {
            "schema": {
              "$ref": "#/components/schemas/TextError"
            },
            "examples": {
              "missing": {
                "summary": "Missing bearer token",
                "value": "Bearer token is required"
              },
              "invalid": {
                "summary": "Invalid bearer token",
                "value": "Invalid bearer token"
              }
            }
          }
        }
      },
      "PluginBearerForbidden": {
        "description": "The credential is valid but is not authorized for the requested account or plugin. The template's plugin-id comparison works only after the route or trusted configuration supplies a plugin identifier; its documented paths do not contain :pluginId.",
        "content": {
          "text/plain": {
            "schema": {
              "$ref": "#/components/schemas/TextError"
            },
            "example": "Token pluginId does not match route pluginId"
          }
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "string",
        "enum": [
          "OK"
        ],
        "description": "Literal process-liveness response returned by the JavaScript template."
      },
      "TextError": {
        "type": "string",
        "description": "Human-readable template error text. App Connect does not parse it as a stable machine error code."
      },
      "PluginRegistrationRequest": {
        "type": "object",
        "required": [
          "rcAccessToken",
          "rcAccountId"
        ],
        "properties": {
          "rcAccessToken": {
            "type": "string",
            "format": "password",
            "minLength": 1,
            "writeOnly": true,
            "description": "Short-lived RingCentral administrator access token. Validate it with RingCentral, compare its account identity with rcAccountId, and never log or retain it."
          },
          "rcAccountId": {
            "type": "string",
            "minLength": 1,
            "description": "Claimed RingCentral account being registered. App Connect sends this value as a string; verify it against the access token."
          }
        },
        "additionalProperties": false,
        "description": "Account registration input sent by the App Connect server to the configured plugin registration URL."
      },
      "PluginRegistrationResponse": {
        "type": "object",
        "required": [
          "jwtToken"
        ],
        "properties": {
          "jwtToken": {
            "type": "string",
            "minLength": 1,
            "description": "Sensitive account-scoped plugin bearer credential. The name is retained for compatibility, but the value can be a JWT or any opaque token understood by the plugin server; App Connect does not inspect its claims."
          }
        },
        "additionalProperties": false,
        "description": "Plugin-issued account credential that App Connect stores for later server-to-server calls."
      },
      "EmptyObject": {
        "type": "object",
        "maxProperties": 0,
        "additionalProperties": false,
        "description": "Empty JSON object used as the token synchronization request body."
      },
      "LoggingPayload": {
        "type": "object",
        "description": "Opaque workflow- and connector-dependent call, SMS, or fax logging payload. It can contain contact details, communication contents, CRM metadata, and recording download URLs with embedded access tokens. Treat it as sensitive and untrusted. A synchronous plugin must preserve every field required by subsequent plugins and connector logging.",
        "additionalProperties": true
      },
      "PluginConfig": {
        "type": "object",
        "nullable": true,
        "description": "User-scoped manifest configuration read from the installed plugin setting. Entries commonly contain nested value and customization metadata. The object can be null or omit values on older or admin-managed clients; validate it and apply server-side defaults.",
        "additionalProperties": true
      },
      "PluginInvocationRequest": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LoggingPayload"
          },
          "config": {
            "$ref": "#/components/schemas/PluginConfig"
          },
          "hashedExtensionId": {
            "type": "string",
            "nullable": true,
            "description": "Deployment-scoped pseudonymous extension identifier supplied for call correlation when available. It can be null and is omitted from current SMS and fax flows. It is not an authentication credential."
          }
        },
        "additionalProperties": false,
        "description": "Envelope sent to a synchronous plugin; the successful response must be the bare LoggingPayload."
      },
      "AsyncCallbackInvocationRequest": {
        "type": "object",
        "required": [
          "data",
          "asyncTaskId",
          "callbackUrl"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LoggingPayload"
          },
          "config": {
            "$ref": "#/components/schemas/PluginConfig"
          },
          "asyncTaskId": {
            "type": "string",
            "format": "uuid",
            "description": "App Connect task UUID for callback-enabled asynchronous call processing."
          },
          "callbackUrl": {
            "type": "string",
            "format": "uri",
            "description": "App Connect completion URL. The embedded task UUID is a secret bearer capability that expires after one week."
          },
          "hashedExtensionId": {
            "type": "string",
            "nullable": true,
            "description": "Deployment-scoped pseudonymous extension identifier supplied for call correlation when available. It can be null and is not an authentication credential."
          }
        },
        "additionalProperties": false,
        "description": "Asynchronous call create or update invocation with a one-week callback task and bearer-capability callback URL."
      },
      "AsyncFireAndForgetInvocationRequest": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LoggingPayload"
          },
          "config": {
            "$ref": "#/components/schemas/PluginConfig"
          }
        },
        "additionalProperties": false,
        "description": "Current asynchronous SMS or fax invocation. It has no task identifier or callback URL, and App Connect does not await its response."
      },
      "AsyncAcceptanceResponse": {
        "type": "object",
        "required": [
          "accepted"
        ],
        "properties": {
          "accepted": {
            "type": "boolean",
            "description": "Template-reported acceptance flag; not inspected by App Connect."
          },
          "asyncTaskId": {
            "type": "string",
            "format": "uuid",
            "description": "Optional echo of the App Connect task UUID for plugin diagnostics; ignored by App Connect."
          },
          "message": {
            "type": "string",
            "description": "Optional plugin diagnostic; ignored by App Connect."
          }
        },
        "additionalProperties": false,
        "description": "JavaScript-template acknowledgment. App Connect does not inspect this body."
      },
      "AsyncRejectedResponse": {
        "type": "object",
        "required": [
          "accepted",
          "message"
        ],
        "properties": {
          "accepted": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always false for this rejection response."
          },
          "message": {
            "type": "string",
            "description": "Human-readable reason for rejection."
          }
        },
        "additionalProperties": false,
        "description": "JavaScript-template rejection when required callback fields are absent."
      },
      "AsyncPluginCallbackRequest": {
        "type": "object",
        "required": [
          "successful"
        ],
        "properties": {
          "successful": {
            "type": "boolean",
            "description": "JSON boolean describing plugin work: true requests a CRM note update; false records task failure."
          },
          "note": {
            "type": "string",
            "description": "Text appended to existing CRM Agent notes when successful is true. Ignored when false."
          },
          "message": {
            "type": "string",
            "description": "Diagnostic result text. Stored as the task failure reason when successful is false; ignored on a successful callback."
          }
        },
        "additionalProperties": true,
        "description": "Plugin completion report for a callback-enabled asynchronous call task."
      },
      "AsyncPluginCallbackResponse": {
        "type": "object",
        "required": [
          "successful"
        ],
        "properties": {
          "successful": {
            "type": "boolean",
            "description": "True when the callback report was recorded, including a plugin-reported failure; false for callback validation or processing errors."
          },
          "message": {
            "type": "string",
            "description": "Human-readable validation or processing error when successful is false."
          }
        },
        "description": "App Connect callback-processing result. successful describes whether the callback was accepted or processed, not whether the plugin background work succeeded."
      },
      "PluginLicenseStatus": {
        "type": "object",
        "required": [
          "licenseStatus"
        ],
        "properties": {
          "licenseStatus": {
            "type": "boolean",
            "description": "True for an active entitlement; false causes the browser to show a license warning."
          },
          "licenseStatusDescription": {
            "type": "string",
            "description": "Detail shown on the plugin configuration page."
          },
          "errorMessage": {
            "type": "string",
            "description": "Short warning shown on the installed-plugin list when licenseStatus is false."
          }
        },
        "additionalProperties": true,
        "description": "Advisory account entitlement result forwarded to the browser UI; it does not currently gate plugin execution."
      },
      "PluginAuthUrlResponse": {
        "type": "object",
        "required": [
          "authUrl"
        ],
        "properties": {
          "authUrl": {
            "type": "string",
            "format": "uri",
            "description": "Absolute provider authorization URL that redirects to the App Connect extension URI and carries validated plugin routing and anti-CSRF state."
          }
        },
        "additionalProperties": false,
        "description": "Preferred JSON response containing the third-party provider authorization URL."
      },
      "PluginAuthStateResponse": {
        "type": "object",
        "required": [
          "successful"
        ],
        "properties": {
          "successful": {
            "type": "boolean",
            "description": "For auth-state checks, whether the correlated plugin user is connected; for logout, whether disconnection succeeded."
          },
          "returnMessage": {
            "$ref": "#/components/schemas/ReturnMessage"
          }
        },
        "additionalProperties": true,
        "description": "Browser-facing result reused for plugin authorization-state checks and logout."
      },
      "PluginLogoutRequest": {
        "type": "object",
        "properties": {
          "jwtToken": {
            "type": "string",
            "format": "password",
            "writeOnly": true,
            "description": "Optional sensitive App Connect CRM-session token sent by the browser client. It is not the plugin bearer; treat it as opaque unless a supported validation path exists."
          }
        },
        "additionalProperties": false,
        "description": "Current browser logout payload containing an optional opaque App Connect CRM-session token."
      },
      "ReturnMessage": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "User-facing status or error message."
          },
          "messageType": {
            "type": "string",
            "description": "Presentation severity. Common values are success, info, warning, danger, and error."
          },
          "ttl": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647,
            "format": "int32",
            "description": "Suggested notification display time in milliseconds."
          },
          "details": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {},
              "additionalProperties": true,
              "description": "Extensible JSON object whose fields are defined by the selected connector or workflow."
            },
            "description": "Optional structured details for clients that can present more than the main message."
          }
        },
        "additionalProperties": true,
        "description": "Optional user-facing notification returned by connector and App Connect workflows."
      }
    }
  }
}
