{
  "openapi": "3.1.0",
  "info": {
    "title": "PushPort External API",
    "version": "1.0.0",
    "description": "External API v1, backend V1 baseline. Android only. 60 requests/minute per API key, maximum 10000 IDs per send, 2 MiB body. Keys cover one app or an account’s accessible apps. The same key must create/read a job. Schedule up to 366 days ahead in Unix milliseconds. Combined title/body/image/click URL UTF-8 budget: 3400 bytes. Duplicates are removed; unknown/foreign IDs are excluded. 202 is not delivery. One current eligible Android subscription per user is selected at dispatch; unavailable subscriptions are skipped. Revocation does not cancel accepted jobs. Account keys (ppa_) cover owned apps and accepted team apps and automatically resolve applications by userId. App keys (pp_) remain supported. Applications are resolved by userId; no App ID is required. Account keys cover owned apps and team apps after invitation acceptance. Key scopes are limited by current membership permissions: viewers can read, senders can send, managers can also update tags. Selected-app grants apply; removal or role changes immediately change key access. Application keys remain restricted to one app.",
    "contact": {
      "email": "support@pushport.dev"
    }
  },
  "servers": [
    {
      "url": "https://pushport.dev"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pp_<43 base64url characters>"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "fields": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "PublicNotification": {
        "type": "object",
        "properties": {
          "userIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10000,
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000
          },
          "imageUrl": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri",
                "maxLength": 1000,
                "description": "Public HTTPS URL, no user info, IP literals or private hostnames; port 443 only"
              },
              {
                "type": "null"
              }
            ]
          },
          "clickUrl": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri",
                "maxLength": 1000,
                "description": "Public HTTPS URL, no user info, IP literals or private hostnames; port 443 only"
              },
              {
                "type": "null"
              }
            ]
          },
          "scheduledAt": {
            "anyOf": [
              {
                "type": "integer",
                "format": "int64",
                "description": "UTC Unix epoch milliseconds"
              },
              {
                "type": "null"
              }
            ]
          },
          "ttlSeconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 604800,
            "default": 86400
          }
        },
        "required": [
          "userIds",
          "title",
          "body"
        ]
      },
      "PartnerPush": {
        "type": "object",
        "properties": {
          "click_uuid": {
            "oneOf": [
              {
                "type": "array",
                "minItems": 1,
                "maxItems": 10000,
                "items": {
                  "type": "string",
                  "minLength": 36,
                  "maxLength": 164,
                  "description": "Original PushPort user UUID without a prefix or suffix"
                }
              },
              {
                "type": "string",
                "minLength": 36,
                "maxLength": 164,
                "description": "Original PushPort user UUID without a prefix or suffix"
              }
            ]
          },
          "header": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000
          },
          "chrome_web_image": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri",
                "maxLength": 1000,
                "description": "Public HTTPS URL, no user info, IP literals or private hostnames; port 443 only"
              },
              {
                "type": "null"
              }
            ]
          },
          "url": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri",
                "maxLength": 1000,
                "description": "Public HTTPS URL, no user info, IP literals or private hostnames; port 443 only"
              },
              {
                "type": "null"
              }
            ]
          },
          "scheduledAt": {
            "anyOf": [
              {
                "type": "integer",
                "format": "int64",
                "description": "UTC Unix epoch milliseconds"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "header",
          "content",
          "click_uuid"
        ]
      },
      "PartnerLookup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 36,
            "maxLength": 164,
            "description": "Original PushPort user UUID without a prefix or suffix"
          }
        },
        "required": [
          "id"
        ]
      },
      "PartnerInstall": {
        "type": "object",
        "properties": {
          "type": {
            "const": "api"
          },
          "installId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "appId": {
            "type": "string",
            "description": "Android package name, not PushPort app UUID"
          },
          "customAppName": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "description": "Language code for compatibility"
          },
          "countryCode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "timezone": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "installId",
          "appId",
          "customAppName",
          "locale",
          "countryCode",
          "timezone"
        ]
      },
      "Install": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "installationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "appId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "packageName": {
            "type": "string"
          },
          "appName": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "countryCode": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "timezone": {
            "type": "string"
          },
          "subscribed": {
            "type": "boolean"
          }
        },
        "required": [
          "userId",
          "installationId",
          "appId",
          "packageName",
          "appName",
          "language",
          "locale",
          "countryCode",
          "timezone",
          "subscribed"
        ]
      },
      "Counts": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "pending": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "accepted": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "failed": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "skipped": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "unknown": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "opened": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        },
        "required": [
          "total",
          "pending",
          "accepted",
          "failed",
          "skipped",
          "unknown",
          "opened"
        ]
      },
      "Notification": {
        "type": "object",
        "properties": {
          "messageId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "running",
              "completed",
              "completed_with_errors",
              "partial",
              "failed",
              "cancelled"
            ]
          },
          "requestedCount": {
            "type": "integer",
            "description": "Unique normalized IDs supplied"
          },
          "queuedCount": {
            "type": "integer",
            "description": "Stored target users, not a delivery count"
          },
          "excludedUserIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          },
          "scheduledAt": {
            "anyOf": [
              {
                "type": "integer",
                "format": "int64",
                "description": "UTC Unix epoch milliseconds"
              },
              {
                "type": "null"
              }
            ]
          },
          "counts": {
            "$ref": "#/components/schemas/Counts"
          },
          "unavailableUsers": {
            "anyOf": [
              {
                "type": "integer",
                "description": "Null before snapshot; targets with no remaining Android subscription afterwards"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "messageId",
          "status",
          "requestedCount",
          "queuedCount",
          "excludedUserIds",
          "scheduledAt",
          "counts",
          "unavailableUsers"
        ]
      }
    }
  },
  "paths": {
    "/api/v1/public/users/{userId}": {
      "get": {
        "operationId": "getUser",
        "summary": "Read latest Android installation metadata; INSTALLATIONS_READ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Install"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          }
        ],
        "description": " Compatibility route: the application is inferred from the key. New integrations should use /api/v1/public/apps/{appId}/... ."
      }
    },
    "/api/v1/public/notifications": {
      "post": {
        "operationId": "sendNotification",
        "summary": "Queue one notification for up to 10000 users; NOTIFICATIONS_WRITE",
        "responses": {
          "202": {
            "description": "Durably queued (not delivered)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Notification"
                    },
                    {
                      "type": "object",
                      "required": [
                        "requestedCount",
                        "queuedCount",
                        "excludedUserIds",
                        "messages"
                      ],
                      "properties": {
                        "requestedCount": {
                          "type": "integer"
                        },
                        "queuedCount": {
                          "type": "integer"
                        },
                        "excludedUserIds": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uuid"
                          }
                        },
                        "messages": {
                          "type": "array",
                          "minItems": 2,
                          "items": {
                            "$ref": "#/components/schemas/Notification"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicNotification"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{1,128}$"
            },
            "description": "Optional. Without this header every request creates a new independent send, including retries. When supplied, reuse it for the same request; changed content returns 409."
          }
        ],
        "description": " Compatibility route: the application is inferred from the key. New integrations should use /api/v1/public/apps/{appId}/... ."
      }
    },
    "/api/v1/public/notifications/{messageId}": {
      "get": {
        "operationId": "getNotification",
        "summary": "Read status using the originating API key; NOTIFICATIONS_READ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Notification"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          }
        ],
        "description": " Compatibility route: the application is inferred from the key. New integrations should use /api/v1/public/apps/{appId}/... ."
      }
    },
    "/api/v1/partner/getInstall/": {
      "post": {
        "operationId": "partnerGetInstall",
        "summary": "Look up one ID; INSTALLATIONS_READ. Trailing slash optional.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerInstall"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerLookup"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "appId",
            "in": "query",
            "required": false,
            "description": "Optional explicit app scope for compatibility. Account keys resolve the application automatically by userId.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/api/v1/partner/push/": {
      "post": {
        "operationId": "partnerPush",
        "summary": "Queue partner payload; NOTIFICATIONS_WRITE. Trailing slash optional.",
        "responses": {
          "202": {
            "description": "Durably queued (not delivered)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Notification"
                    },
                    {
                      "type": "object",
                      "required": [
                        "requestedCount",
                        "queuedCount",
                        "excludedUserIds",
                        "messages"
                      ],
                      "properties": {
                        "requestedCount": {
                          "type": "integer"
                        },
                        "queuedCount": {
                          "type": "integer"
                        },
                        "excludedUserIds": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uuid"
                          }
                        },
                        "messages": {
                          "type": "array",
                          "minItems": 2,
                          "items": {
                            "$ref": "#/components/schemas/Notification"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerPush"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{1,128}$"
            },
            "description": "Optional. Without this header every request creates a new independent send, including retries. When supplied, reuse it for the same request; changed content returns 409."
          },
          {
            "name": "appId",
            "in": "query",
            "required": false,
            "description": "Optional explicit app scope for compatibility. Account keys resolve the application automatically by userId.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/api/v1/public/users/{id}/tags": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Raw PushPort userId, not Android ID, installation ID or a decorated link ID"
        }
      ],
      "get": {
        "summary": "Read user tags",
        "description": "Requires INSTALLATIONS_READ. Access is restricted to the application key or the account’s accessible applications. Compatibility route: the application is inferred from the key. New integrations should use /api/v1/public/apps/{appId}/... .",
        "responses": {
          "400": {
            "description": "Invalid UUID or tags"
          },
          "401": {
            "description": "Invalid or inactive API key"
          },
          "403": {
            "description": "Missing required scope"
          },
          "404": {
            "description": "User not found in this application"
          },
          "429": {
            "description": "Rate limit exceeded; honor Retry-After"
          },
          "200": {
            "description": "Current tags only, without contact details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "userId",
                    "tags",
                    "appId"
                  ],
                  "properties": {
                    "userId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "appId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Merge user tags",
        "description": "Requires USER_TAGS_WRITE. Only supplied tags change. Null deletes a key. Same patch is idempotent; no Idempotency-Key required. Keys and values are case-sensitive. At most 50 resulting tags. Does not create users or trigger pushes. SDK updates and API updates share the same profile. Compatibility route: the application is inferred from the key. New integrations should use /api/v1/public/apps/{appId}/... . The system tag install=\"true\" is assigned automatically regardless of notification permission. It cannot be removed or changed and does not consume a custom tag slot.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tags"
                ],
                "properties": {
                  "tags": {
                    "type": "object",
                    "maxProperties": 50,
                    "propertyNames": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 64
                    },
                    "additionalProperties": {
                      "type": "string",
                      "nullable": true,
                      "maxLength": 256
                    }
                  }
                }
              },
              "example": {
                "tags": {
                  "registration": "true",
                  "purchase": "true"
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Invalid UUID or tags"
          },
          "401": {
            "description": "Invalid or inactive API key"
          },
          "403": {
            "description": "Missing required scope"
          },
          "404": {
            "description": "User not found in this application"
          },
          "429": {
            "description": "Rate limit exceeded; honor Retry-After"
          },
          "200": {
            "description": "Current tags only, without contact details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "userId",
                    "tags",
                    "appId"
                  ],
                  "properties": {
                    "userId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "appId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/public/apps/{appId}/users/{userId}": {
      "get": {
        "operationId": "getUserInApp",
        "summary": "Read latest Android installation metadata; INSTALLATIONS_READ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Install"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          }
        ]
      },
      "parameters": [
        {
          "name": "appId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "PushPort App ID from the dashboard or GET /api/v1/public/app; must be accessible to the API key. Not Firebase App ID or Android package name."
        }
      ]
    },
    "/api/v1/public/apps/{appId}/notifications": {
      "post": {
        "operationId": "sendNotificationInApp",
        "summary": "Queue one notification for up to 10000 users; NOTIFICATIONS_WRITE",
        "responses": {
          "202": {
            "description": "Durably queued (not delivered)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Notification"
                    },
                    {
                      "type": "object",
                      "required": [
                        "requestedCount",
                        "queuedCount",
                        "excludedUserIds",
                        "messages"
                      ],
                      "properties": {
                        "requestedCount": {
                          "type": "integer"
                        },
                        "queuedCount": {
                          "type": "integer"
                        },
                        "excludedUserIds": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uuid"
                          }
                        },
                        "messages": {
                          "type": "array",
                          "minItems": 2,
                          "items": {
                            "$ref": "#/components/schemas/Notification"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicNotification"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{1,128}$"
            },
            "description": "Optional. Without this header every request creates a new independent send, including retries. When supplied, reuse it for the same request; changed content returns 409."
          }
        ]
      },
      "parameters": [
        {
          "name": "appId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "PushPort App ID from the dashboard or GET /api/v1/public/app; must be accessible to the API key. Not Firebase App ID or Android package name."
        }
      ]
    },
    "/api/v1/public/apps/{appId}/notifications/{messageId}": {
      "get": {
        "operationId": "getNotificationInApp",
        "summary": "Read status using the originating API key; NOTIFICATIONS_READ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Notification"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing Idempotency-Key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Scope denied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No resource in this key scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency conflict or Firebase not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (ingress may return non-JSON)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No matching Android users",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds before retry, normally 60"
              }
            }
          },
          "500": {
            "description": "Server error; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unavailable; retry with same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          }
        ]
      },
      "parameters": [
        {
          "name": "appId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "PushPort App ID from the dashboard or GET /api/v1/public/app; must be accessible to the API key. Not Firebase App ID or Android package name."
        }
      ]
    },
    "/api/v1/public/apps/{appId}/users/{id}/tags": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Raw PushPort userId, not Android ID, installation ID or a decorated link ID"
        },
        {
          "name": "appId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "PushPort App ID from the dashboard or GET /api/v1/public/app; must be accessible to the API key. Not Firebase App ID or Android package name."
        }
      ],
      "get": {
        "summary": "Read user tags",
        "description": "Requires INSTALLATIONS_READ. Access is restricted to the application key or the account’s accessible applications.",
        "responses": {
          "400": {
            "description": "Invalid UUID or tags"
          },
          "401": {
            "description": "Invalid or inactive API key"
          },
          "403": {
            "description": "Missing required scope"
          },
          "404": {
            "description": "User not found in this application"
          },
          "429": {
            "description": "Rate limit exceeded; honor Retry-After"
          },
          "200": {
            "description": "Current tags only, without contact details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "userId",
                    "tags",
                    "appId"
                  ],
                  "properties": {
                    "userId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "appId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Merge user tags",
        "description": "Requires USER_TAGS_WRITE. Only supplied tags change. Null deletes a key. Same patch is idempotent; no Idempotency-Key required. Keys and values are case-sensitive. At most 50 resulting tags. Does not create users or trigger pushes. SDK updates and API updates share the same profile. The system tag install=\"true\" is assigned automatically regardless of notification permission. It cannot be removed or changed and does not consume a custom tag slot.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tags"
                ],
                "properties": {
                  "tags": {
                    "type": "object",
                    "maxProperties": 50,
                    "propertyNames": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 64
                    },
                    "additionalProperties": {
                      "type": "string",
                      "nullable": true,
                      "maxLength": 256
                    }
                  }
                }
              },
              "example": {
                "tags": {
                  "registration": "true",
                  "purchase": "true"
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Invalid UUID or tags"
          },
          "401": {
            "description": "Invalid or inactive API key"
          },
          "403": {
            "description": "Missing required scope"
          },
          "404": {
            "description": "User not found in this application"
          },
          "429": {
            "description": "Rate limit exceeded; honor Retry-After"
          },
          "200": {
            "description": "Current tags only, without contact details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "userId",
                    "tags",
                    "appId"
                  ],
                  "properties": {
                    "userId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "appId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/public/app": {
      "get": {
        "summary": "Get the PushPort App ID associated with this API key",
        "description": "Any valid application API key can discover its own App ID. No cross-application listing.",
        "responses": {
          "200": {
            "description": "Application identity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "appId"
                  ],
                  "properties": {
                    "appId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or inactive API key"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    }
  }
}
