{
  "openapi": "3.0.3",
  "info": {
    "title": "U Send News",
    "version": "1",
    "description": "One contract for a customer program and for an MCP connector. A key carries consult, edit, assist, and dispatch. consult reads. edit writes a contact or a draft campaign and never sends. assist proposes and never sends. dispatch queues one letter to one person. A campaign is not sent from this contract. Each key is limited per minute: 60 reads, 30 writes and letter sends, 20 proposals. An unknown bearer shares one counter per address, at most 30 per minute. Over the limit the response is 429, with Retry-After in seconds, x-ratelimit-limit and x-ratelimit-remaining on every answer. Errors are JSON { \"status\": \"<name>\" }. A webhook is not a route of this file: the account registers an https address in the desk. Each notice is a POST of a Notice, signed in the header usn-signature as sha256= and the HMAC-SHA256 of the raw body. The same id is repeated when a notice is retried. campaign names a campaign. letter names a letter. The other is null.\n"
  },
  "servers": [
    {
      "url": "/api/ext/v1"
    }
  ],
  "tags": [
    {
      "name": "account"
    },
    {
      "name": "assist"
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "me",
        "tags": [
          "account"
        ],
        "summary": "Account name, status, key capabilities, and mail and AI balances.",
        "description": "Allowed for a live key on an active or read-only account. No capability is required.",
        "security": [
          {
            "key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account behind the key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "name": "Atelier",
                  "status": "active",
                  "scopes": [
                    "consult",
                    "edit"
                  ],
                  "mail": 1200,
                  "ai": 8000
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/contacts": {
      "post": {
        "operationId": "saveContact",
        "tags": [
          "account"
        ],
        "summary": "Create or update a person.",
        "description": "Requires edit and an active account. A suppressed person is refused. A person who left a list is not put back. A list that asks for confirmation is refused. Nothing is sent. Without a list, the person is stored and does not appear in the subscribed page.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "example": {
                "email": "personne@exemple.fr",
                "givenName": "Ada",
                "familyName": "Martin",
                "list": "a1b2c3d4-e5f6-4789-a012-3456789abcde"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person. status is subscribed, pending, or stored.",
            "content": {
              "application/json": {
                "example": {
                  "id": "e1b2c3d4-e5f6-4789-a012-3456789abcde",
                  "email": "personne@exemple.fr",
                  "status": "subscribed"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "get": {
        "operationId": "contacts",
        "tags": [
          "account"
        ],
        "summary": "Subscribed people, fifty per page.",
        "description": "Requires consult. People who are suppressed are omitted.",
        "security": [
          {
            "key": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of contacts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactPage"
                },
                "example": {
                  "page": 1,
                  "total": 1,
                  "contacts": [
                    {
                      "email": "personne@exemple.fr",
                      "name": "Ada Martin"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/assist": {
      "post": {
        "operationId": "propose",
        "tags": [
          "assist"
        ],
        "summary": "Ask a registered task.",
        "description": "Requires assist and an active account. Draws the AI balance after a successful call. The result stays proposed until a person accepts it in the desk. Nothing is sent. Repeat the same Idempotency-Key to receive the stored proposal again.\n",
        "security": [
          {
            "key": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 80
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Ask"
              },
              "example": {
                "task": "draft_mail",
                "brief": "Une lettre courte sur l'atelier du samedi."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A proposal. Nothing was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Run"
                },
                "example": {
                  "id": "3f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f70",
                  "status": "proposed",
                  "task": "draft_mail",
                  "subject": "Samedi à l'atelier",
                  "html": "<p>Bonjour,</p>",
                  "text": "",
                  "file": ""
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "402": {
            "$ref": "#/components/responses/Empty"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unready"
          }
        }
      }
    },
    "/runs/{id}": {
      "get": {
        "operationId": "run",
        "tags": [
          "assist"
        ],
        "summary": "One proposal of this account.",
        "description": "Requires consult.",
        "security": [
          {
            "key": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The stored proposal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Run"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/Missing"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "operationId": "campaigns",
        "tags": [
          "account"
        ],
        "summary": "Campaigns of the account, newest first.",
        "description": "Requires consult. A page holds fifty campaigns. total is the whole count. Nothing here sends.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of campaigns.",
            "content": {
              "application/json": {
                "example": {
                  "page": 1,
                  "total": 1,
                  "campaigns": [
                    {
                      "id": "6f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f77",
                      "name": "Samedi",
                      "subject": "Samedi à l'atelier",
                      "status": "draft",
                      "created": "2026-11-07T15:02:00Z"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "post": {
        "operationId": "saveCampaign",
        "tags": [
          "account"
        ],
        "summary": "Store a draft campaign.",
        "description": "Requires edit and an active account. The campaign stays a draft. Nothing is sent.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "example": {
                "name": "Samedi",
                "subject": "Samedi à l'atelier",
                "html": "<p>Bonjour,</p>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored draft.",
            "content": {
              "application/json": {
                "example": {
                  "id": "6f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f77",
                  "status": "draft"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/campaigns/{id}": {
      "get": {
        "operationId": "campaign",
        "tags": [
          "account"
        ],
        "summary": "One campaign.",
        "description": "Requires consult. html is present when the campaign was stored as HTML.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign.",
            "content": {
              "application/json": {
                "example": {
                  "id": "6f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f77",
                  "name": "Samedi",
                  "subject": "Samedi à l'atelier",
                  "status": "draft",
                  "created": "2026-11-07T15:02:00Z",
                  "html": "<p>Bonjour,</p>",
                  "text": ""
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/Missing"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/letters": {
      "get": {
        "operationId": "letters",
        "tags": [
          "account"
        ],
        "summary": "Letters ready to send to one person.",
        "description": "Requires consult. A campaign is not listed here. Fifty per page.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of letters.",
            "content": {
              "application/json": {
                "example": {
                  "page": 1,
                  "total": 1,
                  "letters": [
                    {
                      "id": "7f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f77",
                      "name": "Confirmation",
                      "subject": "Votre code"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/letters/{id}": {
      "get": {
        "operationId": "letter",
        "tags": [
          "account"
        ],
        "summary": "One letter and the values it expects.",
        "description": "Requires consult. A required value must be present when the letter is sent.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The letter.",
            "content": {
              "application/json": {
                "example": {
                  "id": "7f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f77",
                  "name": "Confirmation",
                  "subject": "Votre code",
                  "events": [
                    {
                      "key": "CODE",
                      "required": true,
                      "sample": "4821",
                      "html": false
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/Missing"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/letters/{id}/send": {
      "post": {
        "operationId": "sendLetter",
        "tags": [
          "account"
        ],
        "summary": "Queue one letter to one person.",
        "description": "Requires dispatch and an active account. The person is already in the book. A suppressed person is refused. A missing required value is refused. The same Idempotency-Key returns the job already stored and does not queue a second message. The mail balance is drawn when the message leaves. A campaign cannot be sent here.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 80
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "example": {
                "email": "personne@exemple.fr",
                "vars": {
                  "CODE": "4821"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The letter is queued. status stays queued until the engine takes it.",
            "content": {
              "application/json": {
                "example": {
                  "id": "8f1c0a2e-4b7d-4e1a-9c3f-8a2b1d0e5f77",
                  "status": "queued",
                  "email": "personne@exemple.fr"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Invalid"
          },
          "402": {
            "$ref": "#/components/responses/Empty"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/Missing"
          },
          "409": {
            "$ref": "#/components/responses/Held"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/lists": {
      "get": {
        "operationId": "lists",
        "tags": [
          "account"
        ],
        "summary": "Lists and their subscribed people.",
        "description": "Requires consult.",
        "responses": {
          "200": {
            "description": "The lists.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
                    "name": "Atelier",
                    "purpose": "audience",
                    "people": 12
                  }
                ]
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/forms": {
      "get": {
        "operationId": "forms",
        "tags": [
          "account"
        ],
        "summary": "Forms of the account.",
        "description": "Requires consult.",
        "responses": {
          "200": {
            "description": "The forms.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "c1b2c3d4-e5f6-4789-a012-3456789abcde",
                    "name": "Inscription",
                    "status": "live"
                  }
                ]
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/files": {
      "get": {
        "operationId": "files",
        "tags": [
          "account"
        ],
        "summary": "Files of the account.",
        "description": "Requires consult. key is the public key. The storage path is not returned.",
        "responses": {
          "200": {
            "description": "The files.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "d1b2c3d4-e5f6-4789-a012-3456789abcde",
                    "label": "Affiche",
                    "mediaType": "image/png",
                    "byteSize": 12000,
                    "key": "a1b2c3d4e5f64789a0123456789abcde"
                  }
                ]
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/Limited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "key": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "usn_"
      }
    },
    "schemas": {
      "Fault": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "anonymous",
              "forbidden",
              "missing",
              "invalid",
              "empty",
              "unready",
              "unavailable",
              "limited",
              "held"
            ]
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "name",
          "status",
          "scopes",
          "mail",
          "ai"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "read_only",
              "suspended",
              "closed"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "consult",
                "edit",
                "assist",
                "dispatch"
              ]
            }
          },
          "mail": {
            "type": "integer",
            "description": "Remaining mail credits."
          },
          "ai": {
            "type": "integer",
            "description": "Remaining intelligence credits."
          }
        }
      },
      "Contact": {
        "type": "object",
        "required": [
          "email",
          "name"
        ],
        "properties": {
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ContactPage": {
        "type": "object",
        "required": [
          "page",
          "contacts"
        ],
        "properties": {
          "page": {
            "type": "integer"
          },
          "contacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          }
        }
      },
      "Ask": {
        "type": "object",
        "required": [
          "task",
          "brief"
        ],
        "properties": {
          "task": {
            "type": "string",
            "enum": [
              "draft_mail",
              "subjects",
              "alt_text",
              "draft_form",
              "draft_image"
            ]
          },
          "brief": {
            "type": "string"
          }
        }
      },
      "Run": {
        "type": "object",
        "required": [
          "id",
          "status",
          "task",
          "subject",
          "html",
          "text",
          "file"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "proposed",
              "accepted",
              "discarded"
            ]
          },
          "task": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "html": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "file": {
            "type": "string",
            "description": "Public key of a generated image",
            "or empty.": null
          }
        }
      },
      "Notice": {
        "type": "object",
        "required": [
          "id",
          "type",
          "at",
          "email",
          "campaign",
          "letter",
          "detail"
        ],
        "description": "Body of an outbound webhook. campaign names a campaign. letter names a letter. The other is null.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id. Stable across retries."
          },
          "type": {
            "type": "string",
            "enum": [
              "delivered",
              "deferred",
              "hard_bounce",
              "policy_reject",
              "complaint",
              "open",
              "click",
              "unsub",
              "form_submit",
              "automation_entered",
              "automation_sent",
              "scan",
              "page_view",
              "letter_queued"
            ]
          },
          "at": {
            "type": "string",
            "description": "Event time",
            "UTC": null,
            "second precision.": null
          },
          "email": {
            "type": "string"
          },
          "campaign": {
            "type": "string",
            "nullable": true,
            "description": "Campaign id",
            "or null.": null
          },
          "letter": {
            "type": "string",
            "nullable": true,
            "description": "Letter id",
            "or null.": null
          },
          "detail": {
            "type": "object",
            "description": "Only the keys known for this event. link is the clicked address. form, page and qr are the ids of those objects. place, device and client describe where it happened.\n",
            "properties": {
              "link": {
                "type": "string"
              },
              "reason": {
                "type": "string"
              },
              "code": {
                "type": "string"
              },
              "class": {
                "type": "string"
              },
              "form": {
                "type": "string"
              },
              "page": {
                "type": "string"
              },
              "qr": {
                "type": "string"
              },
              "place": {
                "type": "string"
              },
              "device": {
                "type": "string"
              },
              "client": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "responses": {
      "Forbidden": {
        "description": "The key is missing, revoked, expired, or not allowed for this route.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "forbidden"
            }
          }
        }
      },
      "Invalid": {
        "description": "The body, a required letter value, or the idempotency key is not acceptable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "invalid"
            }
          }
        }
      },
      "Empty": {
        "description": "The balance cannot cover this call. A proposal asks nothing of the model. A letter is not queued.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "empty"
            }
          }
        }
      },
      "Missing": {
        "description": "Nothing with this id belongs to the account, or the person is not in the book.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "missing"
            }
          }
        }
      },
      "Limited": {
        "description": "Too many calls. Retry-After says how many seconds to wait. An unknown bearer is counted per address.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "limited"
            }
          }
        }
      },
      "Unready": {
        "description": "No provider is ready for this task, or the service cannot answer.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "unready"
            }
          }
        }
      },
      "Held": {
        "description": "The letter cannot leave yet. The account, the sender, or the domain is not ready.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "held"
            }
          }
        }
      },
      "Unavailable": {
        "description": "The service cannot answer.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fault"
            },
            "example": {
              "status": "unavailable"
            }
          }
        }
      }
    }
  }
}