{
  "openapi": "3.1.0",
  "info": {
    "title": "Postly API",
    "version": "1.1.0",
    "description": "Send fysiske breve i Danmark. Postly printer, kuverterer og afleverer brevet til dao. Beløbet trækkes fra en forudbetalt saldo, eller faktureres månedligt bagud efter aftale. Alle beløb er i øre inkl. 25 % moms. Testnøgler (pk_test_postly_) laver testbreve, som aldrig bliver printet, sendt eller trukket, og som selv bliver afleveret og leveret efter få minutter, så status og webhooks kan afprøves. Se https://postly.dk/api-adgang/dokumentation.",
    "contact": {
      "email": "gustav@postly.dk",
      "url": "https://postly.dk/api-adgang/dokumentation"
    }
  },
  "servers": [
    {
      "url": "https://postly.dk/api/public/v1"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "API-nøgle fra /konto/api: pk_postly_... (rigtig) eller pk_test_postly_... (test)"
      }
    },
    "schemas": {
      "Address": {
        "type": "object",
        "required": [
          "name",
          "address",
          "zip",
          "city"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "company": {
            "type": "string",
            "description": "Firmanavn (valgfri), trykkes over personen"
          },
          "address": {
            "type": "string",
            "description": "Vej og nummer. Uden \"/\": skriv fx \"Vejnavn 23, 2. th\""
          },
          "address2": {
            "type": "string",
            "description": "Att., c/o eller etage (valgfri)"
          },
          "zip": {
            "type": "string",
            "pattern": "^[0-9]{4}$",
            "description": "Dansk postnummer"
          },
          "city": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "LetterCreated": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "paid"
            ]
          },
          "letterType": {
            "type": "string",
            "enum": [
              "almindelig",
              "rekommanderet"
            ]
          },
          "pageCount": {
            "type": "integer"
          },
          "amountOre": {
            "type": "integer"
          },
          "amountFormatted": {
            "type": "string"
          },
          "balanceOre": {
            "type": "integer"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Letter": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Jeres egen reference"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_payment",
              "paid",
              "sent",
              "cancelled",
              "failed"
            ]
          },
          "deliveryStatus": {
            "type": "string",
            "enum": [
              "processing",
              "handed_to_carrier",
              "delivered",
              "attempted",
              "not_collected",
              "cancelled"
            ],
            "description": "Samlet status: processing, så handed_to_carrier, og for rekommanderede breve dao's udfald"
          },
          "letterType": {
            "type": "string",
            "enum": [
              "almindelig",
              "rekommanderet"
            ]
          },
          "pageCount": {
            "type": "integer"
          },
          "amountOre": {
            "type": "integer"
          },
          "recipientName": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "paidAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "trackingNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "dao's indleveringskode (rekommanderet). Testbreve: TEST..."
          },
          "handedToCarrierAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deliveryOutcome": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "delivered",
              "attempted",
              "not_collected",
              null
            ]
          },
          "deliveryReceiptAvailable": {
            "type": "boolean"
          },
          "deliveryReceiptForwardedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deliveryReceiptDetails": {
            "type": "object",
            "description": "Fra dao's kvittering, når den er udfyldt; ellers null",
            "properties": {
              "deliveredAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "place": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "recipient": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "batchId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "livemode": {
            "type": "boolean",
            "description": "false for breve lavet med en testnøgle"
          }
        }
      },
      "DocumentLink": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "active": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "description": "Sendes som POST med headerne Postly-Event og Postly-Signature: t=<unix>,v1=<hex HMAC-SHA256(hemmelighed, t + '.' + body)>. Svar med 2xx. Genforsøg efter 1 min, 5 min, 30 min, 2 t, 12 t og 24 t.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Hændelsens id"
          },
          "event": {
            "type": "string",
            "enum": [
              "letter.handed_to_carrier",
              "letter.delivery_outcome",
              "letter.receipt_available",
              "test.ping"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "livemode": {
            "type": "boolean"
          },
          "data": {
            "$ref": "#/components/schemas/Letter"
          }
        }
      }
    }
  },
  "security": [
    {
      "bearer": []
    }
  ],
  "webhooks": {
    "letterEvent": {
      "post": {
        "summary": "Hændelse for et brev",
        "description": "letter.handed_to_carrier, letter.delivery_outcome eller letter.receipt_available. Tjek Postly-Signature, før I stoler på indholdet.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Modtaget"
          }
        }
      }
    }
  },
  "paths": {
    "/pricing": {
      "get": {
        "summary": "Priser",
        "security": [],
        "responses": {
          "200": {
            "description": "Aktuelle priser i øre inkl. moms"
          }
        }
      }
    },
    "/letters": {
      "post": {
        "summary": "Send et brev",
        "description": "Opretter og sender et brev med det samme. Kan ikke annulleres. Brug Idempotency-Key, så et genforsøg ikke sender brevet to gange.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "letterType",
                  "sender",
                  "recipient"
                ],
                "properties": {
                  "letterType": {
                    "type": "string",
                    "enum": [
                      "almindelig",
                      "rekommanderet"
                    ]
                  },
                  "sender": {
                    "$ref": "#/components/schemas/Address"
                  },
                  "recipient": {
                    "$ref": "#/components/schemas/Address"
                  },
                  "text": {
                    "type": "string",
                    "maxLength": 700000,
                    "description": "Brevets tekst, eller send en fil"
                  },
                  "fileBase64": {
                    "type": "string",
                    "description": "PDF som base64"
                  },
                  "fileName": {
                    "type": "string"
                  },
                  "pageCount": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "description": "Kun brugt, hvis siderne ikke kan tælles"
                  },
                  "reference": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Jeres egen reference, fx lejemål eller sagsnummer"
                  },
                  "testOutcome": {
                    "type": "string",
                    "enum": [
                      "delivered",
                      "attempted",
                      "not_collected"
                    ],
                    "description": "Kun med testnøgle: hvordan testbrevet ender (standard delivered)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Genbrugt svar for samme Idempotency-Key"
          },
          "201": {
            "description": "Brevet er betalt (saldo eller faktura) og sendt til print. Med testnøgle: et testbrev, der aldrig sendes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LetterCreated"
                }
              }
            }
          },
          "400": {
            "description": "Ugyldige data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Manglende eller ugyldig API-nøgle"
          },
          "402": {
            "description": "Saldoen er for lav (aldrig for kunder på faktura)"
          },
          "409": {
            "description": "En anmodning med samme Idempotency-Key behandles eller blev afbrudt"
          },
          "429": {
            "description": "For mange kald"
          }
        }
      },
      "get": {
        "summary": "Seneste breve",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "reference",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 120
            },
            "description": "Kun breve med denne reference"
          }
        ],
        "responses": {
          "200": {
            "description": "Liste over breve, nyeste først",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Letter"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/letters/{id}": {
      "get": {
        "summary": "Status for et brev",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Brevets status og kvitteringsdata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Letter"
                }
              }
            }
          },
          "404": {
            "description": "Ikke fundet"
          }
        }
      }
    },
    "/letters/{id}/receipt": {
      "get": {
        "summary": "dao's kvittering som fil",
        "description": "Et signeret link, der virker i 10 minutter. 404 not_available, indtil kvitteringen findes.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Link til filen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentLink"
                }
              }
            }
          },
          "404": {
            "description": "Brevet eller kvitteringen findes ikke"
          }
        }
      }
    },
    "/letters/{id}/declaration": {
      "get": {
        "summary": "Postlys afsendelseserklæring som fil",
        "description": "Et signeret link, der virker i 10 minutter. Laves, når dao's status sendes videre.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Link til filen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentLink"
                }
              }
            }
          },
          "404": {
            "description": "Brevet eller erklæringen findes ikke"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "summary": "Jeres webhooks",
        "responses": {
          "200": {
            "description": "Liste",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Opret en webhook",
        "description": "Svaret indeholder hemmeligheden (whsec_...) til signaturen. Den vises kun denne ene gang. Højst 5 aktive.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https-adresse"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Oprettet, med secret"
          },
          "400": {
            "description": "Ugyldig adresse eller for mange webhooks"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "delete": {
        "summary": "Slå en webhook fra",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Slået fra"
          },
          "404": {
            "description": "Ikke fundet"
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "summary": "Send en signeret test.ping nu",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "delivered, status og eventuel fejl"
          }
        }
      }
    },
    "/account": {
      "get": {
        "summary": "Konto og saldo",
        "responses": {
          "200": {
            "description": "Konto, saldo i øre, faktureringsform (prepaid eller invoice), aftalte priser ekskl. moms og nøglens type (live eller test)"
          }
        }
      }
    },
    "/topups": {
      "post": {
        "summary": "Fyld saldo op med kort",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amountOre"
                ],
                "properties": {
                  "amountOre": {
                    "type": "integer",
                    "minimum": 10000,
                    "maximum": 5000000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Betalingslink (checkoutUrl) til Stripe"
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "summary": "MCP-server (JSON-RPC 2.0)",
        "description": "Model Context Protocol til AI-assistenter. get_pricing, create_letter_order og get_order_status virker uden nøgle (betaling via link, som brugeren selv betaler); de øvrige værktøjer kræver API-nøgle.",
        "security": [],
        "responses": {
          "200": {
            "description": "JSON-RPC-svar"
          }
        }
      }
    }
  }
}