{
  "openapi": "3.1.0",
  "info": {
    "title": "Plan de Campagne API",
    "version": "1.0.0",
    "summary": "Vrije momenten ophalen en namens een gebruiker een gratis Launch Scan aanvragen of inplannen.",
    "description": "Plan de Campagne maakt launchcampagnes voor het MKB (openingen, productlanceringen, events). De API is openbaar, zonder sleutel of authenticatie. Aanvragen en afspraken gaan pas door als de persoon zelf bevestigt via de mail (link 24 uur geldig).\n\n**Pagina’s lezen:** elke pagina is als Markdown op te vragen met `Accept: text/markdown` of door `.md` achter het pad te zetten (homepage: `/index.md`). Overzicht en wanneer je ons inzet: [llms.txt](https://plandecampagnecontent.nl/llms.txt). Documentatie met voorbeelden: [/docs](https://plandecampagnecontent.nl/docs). Command line: `npx plandecampagne` ([CLI op npm](https://www.npmjs.com/package/plandecampagne)).\n\n**Limieten:** 3 aanvragen per e-mailadres per 24 uur, 10 per IP-adres per uur, 60 leesverzoeken per IP-adres per minuut. Elk antwoord meldt de stand met `RateLimit-Policy` en `RateLimit` (draft-ietf-httpapi-ratelimit-headers); bij 429 staat in `Retry-After` hoe lang je moet wachten.\n\n**Versies:** de API zit onder `/v1`. Brekende wijzigingen komen in een nieuwe versie (`/v2`). Een oude versie blijft na aankondiging nog minstens 6 maanden werken en meldt dat met `Deprecation` (RFC 9745), `Sunset` (RFC 8594) en `Link: <…>; rel=\"successor-version\"`.",
    "contact": {
      "name": "Plan de Campagne",
      "email": "info@plandecampagnecontent.nl",
      "url": "https://plandecampagnecontent.nl"
    }
  },
  "servers": [
    {
      "url": "https://plandecampagnecontent.nl",
      "description": "Productie"
    }
  ],
  "security": [],
  "externalDocs": {
    "description": "Documentatie met voorbeelden",
    "url": "https://plandecampagnecontent.nl/docs"
  },
  "tags": [
    {
      "name": "Planner",
      "description": "Beschikbaarheid voor de gratis Launch Scan."
    },
    {
      "name": "Aanvragen",
      "description": "Launch Scan aanvragen of inplannen namens een gebruiker (met bevestiging per mail)."
    }
  ],
  "paths": {
    "/v1/slots": {
      "get": {
        "operationId": "listLaunchScanSlots",
        "summary": "Vrije momenten voor een Launch Scan",
        "description": "Beschikbare tijdvakken per dag voor een gratis Launch Scan van 30 minuten (tijden in UTC, `label` in Nederlandse tijd). Gebruik een `start` hieruit voor `bookLaunchScan`.",
        "tags": [
          "Planner"
        ],
        "responses": {
          "200": {
            "description": "Beschikbaarheid per dag.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Slots"
                }
              }
            }
          },
          "429": {
            "description": "Te veel leesverzoeken. Wacht het aantal seconden uit `Retry-After`.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "De agenda is tijdelijk niet bereikbaar.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/launch-scan": {
      "post": {
        "operationId": "requestLaunchScan",
        "summary": "Launch Scan aanvragen namens een gebruiker",
        "description": "Vraag een gratis Launch Scan aan zonder vast moment; we nemen binnen een werkdag contact op. De persoon krijgt eerst een mail om te bevestigen, zonder bevestiging gebeurt er niets. Vertel de gebruiker dat die de mail moet checken.",
        "tags": [
          "Aanvragen"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LaunchScanRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aangenomen. De persoon krijgt een bevestigingsmail; pas na bevestiging gaat het door.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pending"
                }
              }
            }
          },
          "400": {
            "description": "Ontbrekend of ongeldig veld (zie `field`).",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Het moment is niet (meer) vrij of de planner staat uit.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limiet bereikt (per e-mailadres of IP-adres). Wacht het aantal seconden uit `Retry-After`.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Onverwachte fout.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings": {
      "post": {
        "operationId": "bookLaunchScan",
        "summary": "Launch Scan inplannen namens een gebruiker",
        "description": "Plan een gratis Launch Scan in op een `start` uit `listLaunchScanSlots`. De persoon krijgt eerst een mail om te bevestigen; het moment wordt pas bij bevestiging vastgezet (en dan opnieuw gecontroleerd).",
        "tags": [
          "Aanvragen"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aangenomen. De persoon krijgt een bevestigingsmail; pas na bevestiging gaat het door.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pending"
                }
              }
            }
          },
          "400": {
            "description": "Ontbrekend of ongeldig veld (zie `field`).",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Het moment is niet (meer) vrij of de planner staat uit.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Limiet bereikt (per e-mailadres of IP-adres). Wacht het aantal seconden uit `Retry-After`.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Onverwachte fout.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimitPolicy": {
        "description": "Quota per policy (draft-ietf-httpapi-ratelimit-headers): `q` aantal, `w` venster in seconden.",
        "schema": {
          "type": "string",
          "example": "\"per-ip\";q=10;w=3600, \"per-email\";q=3;w=86400"
        }
      },
      "RateLimit": {
        "description": "Stand per policy: `r` resterend, `t` seconden tot er weer ruimte is.",
        "schema": {
          "type": "string",
          "example": "\"per-ip\";r=9;t=3600, \"per-email\";r=2;t=86400"
        }
      },
      "RetryAfter": {
        "description": "Seconden wachten voor een nieuwe poging (RFC 9110).",
        "schema": {
          "type": "integer",
          "example": 3600
        }
      }
    },
    "schemas": {
      "Slots": {
        "type": "object",
        "required": [
          "enabled",
          "days",
          "durationMinutes"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "False als de planner tijdelijk uit staat."
          },
          "durationMinutes": {
            "type": "integer",
            "example": 30
          },
          "days": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "date",
                "slots"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date",
                  "example": "2026-10-19"
                },
                "slots": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "start",
                      "end",
                      "label"
                    ],
                    "properties": {
                      "start": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "end": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "label": {
                        "type": "string",
                        "description": "Starttijd in Europe/Amsterdam.",
                        "example": "11:00"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "field": {
            "type": "string",
            "description": "Het veld waar het om gaat (bij 400 en soms 409)."
          }
        }
      },
      "Pending": {
        "type": "object",
        "required": [
          "status",
          "message",
          "expiresAt"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "pending_confirmation"
          },
          "message": {
            "type": "string",
            "description": "Wat je de gebruiker kunt vertellen."
          },
          "summary": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Tot wanneer de bevestigingslink geldig is."
          }
        }
      },
      "LaunchScanRequest": {
        "type": "object",
        "required": [
          "name",
          "email",
          "phone",
          "moment"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "example": "Sanne de Vries"
          },
          "email": {
            "type": "string",
            "format": "email",
            "example": "sanne@voorbeeld.nl"
          },
          "phone": {
            "type": "string",
            "description": "Minimaal 9 cijfers.",
            "example": "0612345678"
          },
          "company": {
            "type": "string",
            "maxLength": 120,
            "example": "Bakkerij De Vries"
          },
          "moment": {
            "type": "string",
            "enum": [
              "opening",
              "product",
              "event",
              "dienst",
              "anders"
            ],
            "description": "Wat er op de planning staat."
          },
          "timing": {
            "type": "string",
            "enum": [
              "0-4",
              "1-3",
              "3-6",
              "later"
            ],
            "description": "Wanneer het moment is (weken/maanden)."
          },
          "contactPreference": {
            "type": "string",
            "enum": [
              "bellen",
              "whatsapp",
              "email"
            ]
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "BookingRequest": {
        "type": "object",
        "required": [
          "start",
          "name",
          "email"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "Een `start` uit `GET /v1/slots`."
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "description": "Verplicht bij `locationType: telefoon`."
          },
          "company": {
            "type": "string",
            "maxLength": 120
          },
          "locationType": {
            "type": "string",
            "enum": [
              "telefoon",
              "meet"
            ],
            "default": "telefoon"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        }
      }
    }
  }
}