KPN

KPN Webhook Signing Keys API

When KPN delivers a webhook to your endpoint, signing keys are to know the request genuinely came from KPN and wasn't tampered with in transit. KPN signs every outbound webhook payload using HMAC-SHA256 and includes the signature in a header. You store a shared secret on your side, compute the expected signature over the incoming request body, and compare it to the header value. If they match, the payload is genuine. This API lets you create and manage those shared secrets. **How signing keys work** You can set a signing key at three levels — Organization, Team, or Application. When KPN delivers a webhook, it picks the active key starting at the most specific level (Application), falling back to Team, then Organization. KPN automatically provisions an organization-level key on your first webhook delivery if you haven't created one. Each key moves through a simple lifecycle: -...

OpenAPI Specification

kpn-webhook-signing-keys-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: Webhook Signing Keys - KPN
  description: |
    When KPN delivers a webhook to your endpoint, signing keys are to know the request genuinely
    came from KPN and wasn't tampered with in transit.

    KPN signs every outbound webhook payload using HMAC-SHA256 and includes the signature
    in a header. You store a shared secret on your side, compute the
    expected signature over the incoming request body, and compare it to the header value.
    If they match, the payload is genuine. This API lets you create and manage those
    shared secrets.

    **How signing keys work**

    You can set a signing key at three levels — Organization, Team, or Application. When
    KPN delivers a webhook, it picks the active key starting at the most specific level
    (Application), falling back to Team, then Organization. KPN automatically provisions
    an organization-level key on your first webhook delivery if you haven't created one.

    Each key moves through a simple lifecycle:

    - **staged** — created but not yet in use. Lets you set up your receiver before switching over.
    - **active** — the key currently used to sign all outgoing webhook deliveries.
    - **retiring** — being phased out after a newer key was activated. Kept alive long enough for any already-sent webhooks to be verified.
    - **retired** — fully decommissioned. Safe to delete.

    **Rotating keys without downtime**

    You can switch to a new signing key at any time without missing a delivery:

    1. Create a new key with `staged: true` — your current active key keeps signing.
    2. Call `/reveal` to get the new key's plaintext secret and configure your receiver to accept both keys.
    3. Activate the new key — the previously active key moves to retiring automatically.
    4. Once you're confident all in-flight deliveries have been verified, retire then delete the old key.

    **Verifying payloads on your end**

    When a webhook arrives, check the header. It contains an HMAC-SHA256
    hex digest of the raw request body, signed with your active key secret. On your side,
    recompute that digest over the raw body bytes using your stored secret and compare the
    two values with a constant-time comparison — this prevents timing attacks. If the values
    don't match, reject the request.

    **Authentication**

    You need an OAuth 2.0 Bearer token to call these endpoints. You must add **WebhookConfigs-SigningKeys-KPN** product to your project. Exchange your **Client ID**
    and **Client Secret** (available in the [KPN API Store](https://developer.kpn.com)) for
    an access token, then pass it as `Authorization: Bearer <token>` on every request.
    Tokens expire, so build in a refresh before expiry.

    **Need help?**

    - [KPN Developer Portal](https://developer.kpn.com)
    - [Getting Started with KPN APIs](https://developer.kpn.com/getting-started)
    - [API Support](https://developer.kpn.com/support)

  version: 4.0.0
  contact:
    name: API Support
    email: api_developer@kpn.com
    url: https://developer.kpn.com/support
  termsOfService: https://developer.kpn.com/legal

servers:
  # Added by API Auto Mocking Plugin
  - description: SwaggerHub API Auto Mocking
    url: https://virtserver.swaggerhub.com/kpn/webhook-signing-key-management-kpn/4.0.0
  - url: https://api-prd.kpn.com/webhookconfigs-kpn
    description: Production

tags:
  - name: Organization Keys
    description: |
      Signing keys at organization scope. Applied to all webhook deliveries in your
      organization unless a team or application key overrides them. KPN auto-provisions
      an organization key on first delivery if none exists. In the next version scopes will be included for Organization level.
  - name: Team Keys
    description: |
      Signing keys at team scope. Override the organization key for all deliveries in
      your team unless an application key is configured.
  - name: Application Keys
    description: |
      Signing keys at application scope. Highest precedence — overrides both team and
      org keys for deliveries belonging to this specific application.

security:
  - OAuth2: []

paths:

  # ─── Organization Keys ────────────────────────────────────────────────────────

  /organizations/keys:
    post:
      operationId: createOrgSigningKey
      tags: [Organization Keys]
      summary: Create organization signing key
      description: |
        Creates a new signing key at organization scope.

        - `staged: false` (default) — key becomes **active** immediately; any previously
          active org key is moved to **retiring** automatically.
        - `staged: true` — key is created as **staged**; activate it later via
          `POST /organizations/keys/{key_id}/activate`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SigningKeyCreateRequest'
            examples:
              activeKey:
                summary: Create and immediately activate
                value:
                  secret: my-shared-secret-at-least-32-chars-long
              stagedKey:
                summary: Create in staged state (activate later)
                value:
                  secret: my-shared-secret-at-least-32-chars-long
                  staged: true
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      operationId: listOrgSigningKeys
      tags: [Organization Keys]
      summary: List organization signing keys
      description: Lists all signing keys at organization scope. Secrets are never included — use `/reveal` for the active key's plaintext.
      parameters:
        - $ref: '#/components/parameters/StatusFilter'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SigningKeyResponse'
              example:
                - key_id: wk_2C6EDCDFDFD944F3A75D0D36
                  status: active
                  created_at: '2026-06-10T11:56:04.136Z'
                  updated_at: '2026-06-10T11:56:04.361Z'
                - key_id: wk_1A3BCDEFFE2311E0B94E0A25
                  status: retiring
                  created_at: '2026-05-01T08:00:00.000Z'
                  updated_at: '2026-06-10T11:56:04.361Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /organizations/keys/reveal:
    get:
      operationId: revealOrgSigningKeySecret
      tags: [Organization Keys]
      summary: Reveal active org signing key secret
      description: |
        Returns the **decrypted plaintext secret** for the currently active
        organization signing key.

        Use this to configure your webhook receiver for HMAC-SHA256 signature
        verification. Treat this value like a password — never log or expose it.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyRevealResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No active signing key found for your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /organizations/keys/{key_id}/activate:
    post:
      operationId: activateOrgSigningKey
      tags: [Organization Keys]
      summary: Activate a staged org signing key
      description: |
        Transitions the key from `staged` to `active`.
        Any currently `active` org key is automatically moved to `retiring`.

        Only keys in `staged` state can be activated.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '200':
          description: Activated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          description: Key is not in staged state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /organizations/keys/{key_id}/retire:
    post:
      operationId: retireOrgSigningKey
      tags: [Organization Keys]
      summary: Retire a retiring org signing key
      description: |
        Transitions the key from `retiring` to `retired`.

        Wait until all in-flight webhook deliveries signed with this key have been
        verified before retiring it. Retiring keys can still be used for verification
        but will not sign new deliveries.

        Only keys in `retiring` state can be retired.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '200':
          description: Retired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          description: Key is not in retiring state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /organizations/keys/{key_id}:
    delete:
      operationId: deleteOrgSigningKey
      tags: [Organization Keys]
      summary: Delete a retired org signing key
      description: |
        Permanently deletes the signing key record.
        Only keys in `retired` state can be deleted.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '204':
          description: Deleted
        '400':
          description: Key is not in retired state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ─── Team Keys ────────────────────────────────────────────────────────────────

  /teams/keys:
    post:
      operationId: createTeamSigningKey
      tags: [Team Keys]
      summary: Create team signing key
      description: |
        Creates a new signing key at team scope.

        - `staged: false` (default) — key becomes **active** immediately; any previously
          active team key is moved to **retiring** automatically.
        - `staged: true` — key is created as **staged**; activate it later.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SigningKeyCreateRequest'
            examples:
              activeKey:
                summary: Create and immediately activate
                value:
                  secret: my-shared-secret-at-least-32-chars-long
              stagedKey:
                summary: Create in staged state
                value:
                  secret: my-shared-secret-at-least-32-chars-long
                  staged: true
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      operationId: listTeamSigningKeys
      tags: [Team Keys]
      summary: List team signing keys
      description: Lists all signing keys at team scope.
      parameters:
        - $ref: '#/components/parameters/StatusFilter'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SigningKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /teams/keys/reveal:
    get:
      operationId: revealTeamSigningKeySecret
      tags: [Team Keys]
      summary: Reveal active team signing key secret
      description: Returns the decrypted plaintext secret for the currently active team signing key.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyRevealResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No active signing key found for your team.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /teams/keys/{key_id}/activate:
    post:
      operationId: activateTeamSigningKey
      tags: [Team Keys]
      summary: Activate a staged team signing key
      description: |
        Transitions the key from `staged` to `active`.
        Any currently `active` team key is automatically moved to `retiring`.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '200':
          description: Activated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          description: Key is not in staged state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /teams/keys/{key_id}/retire:
    post:
      operationId: retireTeamSigningKey
      tags: [Team Keys]
      summary: Retire a retiring team signing key
      description: Transitions the key from `retiring` to `retired`. Only keys in `retiring` state can be retired.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '200':
          description: Retired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          description: Key is not in retiring state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /teams/keys/{key_id}:
    delete:
      operationId: deleteTeamSigningKey
      tags: [Team Keys]
      summary: Delete a retired team signing key
      description: Permanently deletes the key record. Only keys in `retired` state can be deleted.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '204':
          description: Deleted
        '400':
          description: Key is not in retired state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ─── Application Keys ─────────────────────────────────────────────────────────

  /applications/keys:
    post:
      operationId: createApplicationSigningKey
      tags: [Application Keys]
      summary: Create application signing key
      description: |
        Creates a new signing key at application scope.

        - `staged: false` (default) — key becomes **active** immediately; any previously
          active application key is moved to **retiring** automatically.
        - `staged: true` — key is created as **staged**; activate it later.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SigningKeyCreateRequest'
            examples:
              activeKey:
                summary: Create and immediately activate
                value:
                  secret: my-shared-secret-at-least-32-chars-long
              stagedKey:
                summary: Create in staged state
                value:
                  secret: my-shared-secret-at-least-32-chars-long
                  staged: true
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      operationId: listApplicationSigningKeys
      tags: [Application Keys]
      summary: List application signing keys
      description: Lists all signing keys at application scope.
      parameters:
        - $ref: '#/components/parameters/StatusFilter'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SigningKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /applications/keys/reveal:
    get:
      operationId: revealApplicationSigningKeySecret
      tags: [Application Keys]
      summary: Reveal active application signing key secret
      description: Returns the decrypted plaintext secret for the currently active application signing key.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyRevealResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No active signing key found for your application.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /applications/keys/{key_id}/activate:
    post:
      operationId: activateApplicationSigningKey
      tags: [Application Keys]
      summary: Activate a staged application signing key
      description: |
        Transitions the key from `staged` to `active`.
        Any currently `active` application key is automatically moved to `retiring`.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '200':
          description: Activated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          description: Key is not in staged state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /applications/keys/{key_id}/retire:
    post:
      operationId: retireApplicationSigningKey
      tags: [Application Keys]
      summary: Retire a retiring application signing key
      description: Transitions the key from `retiring` to `retired`. Only keys in `retiring` state can be retired.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '200':
          description: Retired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyResponse'
        '400':
          description: Key is not in retiring state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /applications/keys/{key_id}:
    delete:
      operationId: deleteApplicationSigningKey
      tags: [Application Keys]
      summary: Delete a retired application signing key
      description: Permanently deletes the key record. Only keys in `retired` state can be deleted.
      parameters:
        - $ref: '#/components/parameters/KeyId'
      responses:
        '204':
          description: Deleted
        '400':
          description: Key is not in retired state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  parameters:
    KeyId:
      in: path
      name: key_id
      required: true
      schema:
        type: string
      description: The `key_id` returned by the create or list endpoint.
      example: wk_2C6EDCDFDFD944F3A75D0D36
    ApplicationName:
      in: header
      name: x-application-name
      required: false
      schema:
        type: string
      description: Human-readable name for your application. Optional; used for display purposes only.
    StatusFilter:
      in: query
      name: status
      required: false
      description: Filter keys by lifecycle status.
      schema:
        type: string
        enum: [STAGED, ACTIVE, RETIRING, RETIRED]

  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://api-prd.kpn.com/oauth/client_credential/accesstoken?grant_type=client_credentials
          scopes: {}

  responses:
    BadRequest:
      description: Bad request — check your request body for missing or invalid fields.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            errorResponse:
              code: "400"
              message: "Bad Request"
              info: "https://developer.kpn.com/documentation-webhook-signing-keys"
    Unauthorized:
      description: Unauthorized — missing or invalid Bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            errorResponse:
              code: "401"
              message: "Unauthorized"
              info: "https://developer.kpn.com/getting-started"
    Forbidden:
      description: Forbidden — your token does not have permission to access this resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            errorResponse:
              code: "403"
              message: "Forbidden"
              info: "https://developer.kpn.com/support"
    NotFound:
      description: Not found — the requested key does not exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            errorResponse:
              code: "404"
              message: "Not Found"
              info: "https://developer.kpn.com/documentation-webhook-signing-keys"
    InternalServerError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
          example:
            errorResponse:
              code: "500"
              message: "Internal Server Error"
              info: "https://developer.kpn.com/support"

  schemas:

    ApiErrorResponse:
      type: object
      properties:
        errorResponse:
          type: object
          properties:
            code:
              type: string
              description: HTTP status code as a string.
              example: "400"
            message:
              type: string
              description: Human-readable error message.
            info:
              type: string
              description: URL to relevant documentation or support page.

    SigningKeyCreateRequest:
      type: object
      required: [secret]
      properties:
        key_id:
          type: string
          maxLength: 128
          description: |
            Optional custom identifier for this key. If omitted, a `wk_`-prefixed ID is
            generated automatically (e.g. `wk_2C6EDCDFDFD944F3A75D0D36`).
        secret:
          type: string
          minLength: 1
          maxLength: 4096
          description: |
            The shared secret used for HMAC-SHA256 signing of outbound webhook payloads.
            Configure your webhook receiver with this value to verify incoming signatures.
            **This value is never returned after creation** — use the `/reveal` endpoint
            to retrieve it for the currently active key.
        staged:
          type: boolean
          default: false
          description: |
            `false` (default) — key becomes active immediately; any previously active key
            at the same scope is moved to retiring automatically.

            `true` — key is created in staged state. Use this for zero-downtime rotation:
            update your receiver first, then activate the key.

    SigningKeyResponse:
      type: object
      description: Signing key metadata. Does not include the secret value.
      properties:
        key_id:
          type: string
          example: wk_2C6EDCDFDFD944F3A75D0D36
        status:
          type: string
          enum: [staged, active, retiring, retired]
          description: Current lifecycle state of the key.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SigningKeyRevealResponse:
      type: object
      description: |
        Contains the decrypted plaintext secret for the active signing key.
        **Treat this like a password** — do not log or expose it.
      properties:
        key_id:
          type: string
          example: wk_2C6EDCDFDFD944F3A75D0D36
        secret:
          type: string
          description: Plaintext HMAC-SHA256 shared secret. Configure your receiver with this value.
        status:
          type: string
          enum: [active]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time