{
  "openapi": "3.1.0",
  "info": {
    "title": "notifi",
    "summary": "Push notifications to an iPhone, iPad or Mac from one HTTP request.",
    "description": "notifi delivers a push notification to the device that created the send key you authenticate with. There is no account. Install the app, copy the send key it makes on first launch, and post a title and a body to notifi.it. Notification content is encrypted with the device’s public key before the server stores it, so neither notifi nor Apple can read your notifications.\n\nOnly the device can mint a send key, with a request signed by a private key that never leaves it. There is no endpoint that creates one, so an agent has to ask a human to copy the key out of the app’s Keys tab.\n\nWhen to use it: a build, backup or training run that finished or failed; a CI job or deploy that broke; a coding agent that is done or is blocked on a decision; a cron job or home server that noticed something. It cannot reach anyone who has not given you one of their own keys, and delivery is best-effort, so do not make it the only path for anything where a missed notification causes harm.\n\nUse of the API is use of the service, and the terms at notifi.it/terms apply to it.",
    "version": "1.0.0",
    "termsOfService": "https://notifi.it/terms",
    "license": {
      "name": "MIT",
      "url": "https://github.com/notifi-it/notifi/blob/main/LICENSE"
    },
    "contact": {
      "name": "notifi",
      "url": "https://notifi.it",
      "email": "hello@notifi.it"
    }
  },
  "externalDocs": {
    "description": "notifi API documentation",
    "url": "https://notifi.it/docs"
  },
  "servers": [
    {
      "url": "https://notifi.it",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "send",
      "description": "Delivering a notification."
    }
  ],
  "paths": {
    "/send": {
      "get": {
        "tags": [
          "send"
        ],
        "operationId": "sendNotificationViaQuery",
        "summary": "Send a notification with query parameters",
        "description": "The same operation as POST /send, for a quick test from a browser or a shell. A key in a query string ends up in edge logs and shell history, so prefer POST with an Authorization header, and rotate any key you have sent this way.",
        "security": [
          {
            "sendKey": []
          },
          {
            "keyParameter": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/key"
          },
          {
            "$ref": "#/components/parameters/title"
          },
          {
            "$ref": "#/components/parameters/message"
          },
          {
            "$ref": "#/components/parameters/link"
          },
          {
            "$ref": "#/components/parameters/image"
          },
          {
            "$ref": "#/components/parameters/occurred_at"
          },
          {
            "$ref": "#/components/parameters/is_critical"
          }
        ],
        "responses": {
          "202": {
            "$ref": "#/components/responses/Accepted"
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/UnknownKey"
          },
          "422": {
            "$ref": "#/components/responses/InvalidContent"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      },
      "post": {
        "tags": [
          "send"
        ],
        "operationId": "sendNotification",
        "summary": "Send a notification",
        "description": "Delivers a notification to the device that created the send key. Answers 202 once the server has accepted it, which is not a delivery receipt. Query parameters win over body fields when both are present. The key body field also authenticates; the security schemes above cannot describe a credential in the body, so it is listed only as a field of SendParams.",
        "security": [
          {
            "sendKey": []
          },
          {
            "keyParameter": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendParams"
              },
              "example": {
                "title": "Hello from notifi",
                "message": "Your first notification.",
                "link": "https://notifi.it/docs"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/SendParams"
              },
              "example": {
                "title": "Hello from notifi",
                "message": "Your first notification.",
                "link": "https://notifi.it/docs"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/SendParams"
              },
              "example": {
                "title": "Hello from notifi",
                "message": "Your first notification.",
                "link": "https://notifi.it/docs"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Accepted"
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/UnknownKey"
          },
          "422": {
            "$ref": "#/components/responses/InvalidContent"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sendKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The send key from the app’s Keys tab, as Authorization: Bearer nk_yourkey. Preferred: edge logs and shell history do not record a header."
      },
      "keyParameter": {
        "type": "apiKey",
        "in": "query",
        "name": "key",
        "description": "The send key as a parameter. It appears in edge logs, in shell history and in any proxy in between, which makes it the weaker option. Use it only for a quick test, and rotate the key afterwards."
      }
    },
    "parameters": {
      "key": {
        "name": "key",
        "in": "query",
        "required": false,
        "description": "The send key, unless you send it as a bearer token.",
        "schema": {
          "type": "string",
          "pattern": "^nk_"
        },
        "example": "nk_yourkey"
      },
      "title": {
        "name": "title",
        "in": "query",
        "required": true,
        "description": "The notification title.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        },
        "example": "Hello from notifi"
      },
      "message": {
        "name": "message",
        "in": "query",
        "required": false,
        "description": "The notification body, in Markdown.",
        "schema": {
          "type": "string",
          "maxLength": 16000
        },
        "example": "Your first notification."
      },
      "link": {
        "name": "link",
        "in": "query",
        "required": false,
        "description": "A link to a website or internal app.",
        "schema": {
          "type": "string",
          "maxLength": 2048,
          "format": "uri"
        },
        "example": "https://notifi.it/docs"
      },
      "image": {
        "name": "image",
        "in": "query",
        "required": false,
        "description": "URL of an image shown with the notification.",
        "schema": {
          "type": "string",
          "maxLength": 2048,
          "format": "uri"
        },
        "example": "https://notifi.it/anaglyph-bell.png"
      },
      "occurred_at": {
        "name": "occurred_at",
        "in": "query",
        "required": false,
        "description": "When the event happened, as unix milliseconds.",
        "schema": {
          "type": "integer",
          "minimum": 946684800000,
          "format": "int64"
        }
      },
      "is_critical": {
        "name": "is_critical",
        "in": "query",
        "required": false,
        "description": "Breaks through Focus.",
        "schema": {
          "type": "boolean"
        }
      }
    },
    "schemas": {
      "SendParams": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^nk_",
            "description": "The send key, unless you send it as a bearer token. The key picks the device that receives the notification. Two keys, comma-separated, send to two devices in one request, one per platform.",
            "examples": [
              "nk_yourkey"
            ]
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The notification title. notifi crops a longer title and adds a warning to the response.",
            "examples": [
              "Hello from notifi"
            ]
          },
          "message": {
            "type": "string",
            "maxLength": 16000,
            "description": "The notification body, in Markdown. notifi crops a longer body and adds a warning.",
            "examples": [
              "Your first notification."
            ]
          },
          "link": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "A link to a website or internal app. Opens when you tap the notification. https always opens; another scheme (shortcuts://run-shortcut?name=Deploy, or an app’s own deep link) opens only when the key’s Open any link switch is on in the app, and the app hides the link otherwise.",
            "examples": [
              "https://notifi.it/docs"
            ]
          },
          "image": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "URL of an image shown with the notification. The receiving device fetches it; the server never does. By default the app loads it only when you tap it.",
            "examples": [
              "https://notifi.it/anaglyph-bell.png"
            ]
          },
          "occurred_at": {
            "type": "integer",
            "minimum": 946684800000,
            "format": "int64",
            "description": "When the event happened, as unix milliseconds. For a queued or retried send. Only changes the timestamp shown in the app; defaults to the time the server accepted the request."
          },
          "is_critical": {
            "type": "boolean",
            "description": "Breaks through Focus. The key must also have urgent alerts switched on in the app, or notifi delivers an ordinary notification."
          }
        },
        "additionalProperties": false
      },
      "SendResponse": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "sent": {
            "type": "integer",
            "minimum": -9007199254740991,
            "description": "Present only for two keys: how many devices received the notification."
          },
          "results": {
            "maxItems": 2,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                },
                "error": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "bad_signature",
                        "stale_timestamp",
                        "unknown_device",
                        "unknown_key",
                        "rate_limited",
                        "uncollected_limit",
                        "invalid_request",
                        "invalid_content",
                        "not_found",
                        "internal_error"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "code",
                    "message"
                  ],
                  "additionalProperties": false
                },
                "warnings": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "key",
                "ok"
              ],
              "additionalProperties": false
            },
            "description": "Present only for two keys: one entry per key, in the order given, named by the key prefix the Keys tab shows."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only when notifi delivered the notification differently from the request: a cropped title or body, or is_critical on a key without urgent alerts."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unknown_key",
                  "rate_limited",
                  "uncollected_limit",
                  "invalid_request",
                  "invalid_content",
                  "not_found",
                  "internal_error"
                ],
                "description": "Read error.code, not code: the code sits one level down."
              },
              "message": {
                "type": "string",
                "description": "A readable explanation, in the language negotiated from Accept-Language."
              }
            }
          }
        }
      }
    },
    "responses": {
      "Accepted": {
        "description": "The server accepted the notification; it is not a delivery receipt.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/SendResponse"
            },
            "examples": {
              "delivered": {
                "value": {
                  "ok": true
                }
              },
              "deliveredWithWarnings": {
                "value": {
                  "ok": true,
                  "warnings": [
                    "Title shortened to 200 characters."
                  ]
                }
              },
              "deliveredToOneOfTwo": {
                "value": {
                  "ok": true,
                  "sent": 1,
                  "results": [
                    {
                      "key": "nk_abcd",
                      "ok": true
                    },
                    {
                      "key": "nk_wxyz",
                      "ok": false,
                      "error": {
                        "code": "rate_limited",
                        "message": "Not sent. Daily limit of 25 notifications reached. Resets at midnight UTC."
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "UnexpectedError": {
        "description": "Any other failure, in the same error shape: uncollected_limit or not_found or internal_error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "A parameter is missing or malformed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "title is required."
              }
            }
          }
        }
      },
      "UnknownKey": {
        "description": "The key is unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unknown_key",
                "message": "Unknown or revoked key."
              }
            }
          }
        }
      },
      "InvalidContent": {
        "description": "The device is set to refuse a notification it cannot deliver as written.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_content",
                "message": "Not sent. This device is set to refuse a notification it cannot deliver as written."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over the daily device limit or the per-minute IP limit. Carries a Retry-After header with the seconds until the window resets.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Not sent. Daily limit of 25 notifications reached. Resets at midnight UTC."
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds until the window resets.",
            "schema": {
              "type": "integer"
            }
          }
        }
      }
    }
  },
  "x-rate-limits": {
    "perDevicePerDay": 25,
    "perIpPerMinute": 100
  }
}
