Webhook configuration

OSA Proxy sends HTTP POST notifications when files are downloaded or blocked. Webhook delivery is outside the client request path: a download does not wait for the receiver, and a delivery error does not change the OSA Proxy response.

Notifications are available for all supported ecosystems except Docker. They are created only for GET requests to repository routes. HEAD requests, operational endpoints, and ordinary upstream errors do not create notifications.

Events

EventWhen it is sent
file_downloadedOSA Proxy returned a file with status 200 or 206 without a completed CodeScoring evaluation. A subscription to this event covers all successful downloads, including evaluated ones. For an evaluated download, the notification uses the scanned_file_downloaded trigger.
scanned_file_downloadedOSA Proxy returned a file with status 200 or 206 after a completed CodeScoring evaluation, including a cached verdict.
file_blockedOSA Proxy explicitly blocked the request. The scanned field indicates whether a completed CodeScoring verdict was available at that point.

If one subscription contains both file_downloaded and scanned_file_downloaded, an evaluated request produces only one notification with trigger: scanned_file_downloaded.

A 200 or 206 status is the response observed by OSA Proxy. It does not confirm that the client received or saved the complete file.

Request format

Each POST contains exactly one event:

{
  "events": [
    {
      "created_at": "2026-09-10T10:15:30.123456Z",
      "trigger": "scanned_file_downloaded",
      "payload": {
        "scanned": true,
        "reason": "codescoring evaluation completed",
        "repository": "company-npm",
        "ecosystem": "npm",
        "object_kind": "package",
        "path": "/company-npm/lodash/-/lodash-4.17.21.tgz",
        "purl": "pkg:npm/lodash@4.17.21"
      }
    }
  ]
}

Payload fields:

FieldDescription
scannedtrue when CodeScoring evaluation completed.
reasonResult reason, such as completed, blocked, disabled, or bypassed evaluation. This diagnostic value may be extended.
repositoryPublic repository route name. The repositories filter compares this exact value.
ecosystemPackage ecosystem, such as npm, maven, or pypi.
object_kindObject type known to OSA Proxy: file, package, or manifest.
pathCanonical escaped path of the original request without the query string.
purlPackage PURL when OSA Proxy can determine it. Otherwise, the field is absent.

OSA Proxy sends Content-Type: application/json. When a secret is configured, it also sends X-CodeScoring-Authentication. There is no separate request-signature header.

The webhook continues the original request trace. The receiver gets trace headers and the same X-Trace-Id used for upstream and CodeScoring requests.

A test POST differs from an operational notification and checks connectivity only:

{
  "events": [
    {
      "created_at": "2026-09-10T10:15:30.123456Z",
      "trigger": "test",
      "payload": {}
    }
  ]
}

The receiver can return any 2xx status. OSA Proxy treats the delivery as successful and ignores the response body.

Configuration in osa-proxy.yml

Without Configuration Store, add subscriptions at the root of osa-proxy.yml. Set token_env to the name of the environment variable containing the token:

webhooks:
  - name: downloads
    url: https://hooks.example.com/osa-proxy/events
    token_env: WEBHOOK_DOWNLOADS_TOKEN
    enabled: true
    events:
      - file_downloaded
      - scanned_file_downloaded
      - file_blocked
    repositories:
      - company-npm
      - company-maven
    timeout: 3s
    buffer_size: 256

Restart every OSA Proxy replica after changing YAML. Give every replica the same subscription and token configuration for consistent behavior.

Subscription parameters

ParameterRequirement and behavior
nameHuman-readable name. Required in managed mode.
urlAbsolute receiver http or https URL. Managed mode rejects credentials in the URL.
token_envOptional name of the environment variable containing the receiver token. The token is sent in X-CodeScoring-Authentication but is not stored in configuration.
enabledEnables the subscription. Defaults to true when absent in YAML; required by the managed API.
eventsNon-empty list of supported events.
repositoriesExact, case-sensitive list of public repository names. An empty or absent list accepts events from every non-Docker repository.
timeoutComplete timeout for one POST. Default is 3s; managed mode accepts values from 1ns through 30s.
buffer_sizeSize of this subscription's queue. Default is 256; managed mode accepts values from 1 through 4096.

Each enabled subscription has its own bounded queue and one sequential sender. When the queue is full, the new notification is dropped. Every 30 seconds, OSA Proxy writes the accumulated count to the webhook notifications dropped warning log.

Only one delivery attempt is made for each notification. There are no retries or durable storage. Redirects are not followed, so a 3xx response is a delivery failure. Non-2xx responses, timeouts, and network errors produce a webhook delivery failed warning without affecting the client request.

Best-effort delivery

A notification can be lost when a queue is full, the process stops, or the receiver fails. Do not use a webhook as the only audit source or as transactional confirmation of a download.

Receiver authentication

Create a Secret with the receiver token and pass it to OSA Proxy through an environment variable. Then put the variable name in token_env:

webhooks:
  - name: downloads
    url: https://hooks.example.com/osa-proxy/events
    token_env: WEBHOOK_DOWNLOADS_TOKEN

Do not put the token value in configuration or the repository. In managed mode, send token_env through the Admin API with the subscription; you do not need to duplicate the webhook in osa-proxy.yml. The API stores and returns only the variable name, never the token.

If the variable is missing or empty, the subscription is not activated. Restart every OSA Proxy replica after adding or changing the Secret.

Connectivity test through Admin API

Admin API uses a separate listener, 127.0.0.1:8081 by default, and a Bearer Admin Token. In Kubernetes, do not expose the admin Service without TLS, an authenticated ingress or reverse proxy, and network restrictions.

You can test an unsaved subscription whether Configuration Store is enabled or not:

curl --fail-with-body \
  -H "Authorization: Bearer ${OSA_PROXY_ADMIN_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "qa-receiver",
    "url": "https://hooks.example.com/osa-proxy/events",
    "token_env": "WEBHOOK_DOWNLOADS_TOKEN",
    "enabled": true,
    "events": ["file_blocked"],
    "repositories": [],
    "timeout": "3s",
    "buffer_size": 16
  }' \
  http://127.0.0.1:8081/api/v1/configuration/webhooks/test

Example successful result:

{"success":true,"status_code":204,"duration_ms":18}

success: true means only that the test POST received a 2xx response. Possible error_category values are invalid_configuration, timeout, connection, and http_status.

Managing subscriptions through Admin API

Subscription CRUD is available only when Configuration Store is enabled. With storage disabled, these endpoints return 503 managed_storage_disabled; use YAML instead. Swagger UI is available at /swagger/, and OpenAPI at /openapi.json on the admin listener.

Main endpoints:

Method and pathPurpose
GET /api/v1/configuration/webhooks/triggersList supported events.
GET /api/v1/configuration/webhooksReturn all subscriptions and the current section ETag.
PUT /api/v1/configuration/webhooksReplace all subscriptions. Each item requires an id.
POST /api/v1/configuration/webhooksCreate a subscription. Do not provide id; the server generates it.
GET /api/v1/configuration/webhooks/{id}Return one subscription.
PUT /api/v1/configuration/webhooks/{id}Completely replace one subscription.
PATCH /api/v1/configuration/webhooks/{id}Change one or more fields. null, an empty object, and unknown fields are rejected.
DELETE /api/v1/configuration/webhooks/{id}Delete a subscription.
POST /api/v1/configuration/webhooks/testTest a request-body subscription without saving it.
POST /api/v1/configuration/webhooks/{id}/testTest a saved subscription. No request body is required.

Every mutation uses optimistic concurrency. Get the section ETag first, then pass it unchanged in If-Match:

curl -i \
  -H "Authorization: Bearer ${OSA_PROXY_ADMIN_TOKEN}" \
  http://127.0.0.1:8081/api/v1/configuration/webhooks

Copy the ETag header value, including its double quotes:

curl --fail-with-body \
  -H "Authorization: Bearer ${OSA_PROXY_ADMIN_TOKEN}" \
  -H 'Content-Type: application/json' \
  -H 'If-Match: "section-webhooks-1:sha256:REPLACE_WITH_ACTUAL_HASH"' \
  -d '{
    "name": "blocked-downloads",
    "url": "https://hooks.example.com/osa-proxy/events",
    "token_env": "WEBHOOK_DOWNLOADS_TOKEN",
    "enabled": true,
    "events": ["file_blocked"],
    "repositories": ["company-npm"],
    "timeout": "3s",
    "buffer_size": 256
  }' \
  http://127.0.0.1:8081/api/v1/configuration/webhooks

A successful mutation returns a new ETag. Reusing an old ETag returns 409 section_conflict; repeat GET, inspect the current state, and then deliberately retry the change. Request body fields are validated strictly, and the section is limited to 100 subscriptions.

Testing a saved subscription does not create a revision:

curl --fail-with-body -X POST \
  -H "Authorization: Bearer ${OSA_PROXY_ADMIN_TOKEN}" \
  http://127.0.0.1:8081/api/v1/configuration/webhooks/WEBHOOK_ID/test

The response contains section_etag, which lets a client detect whether the section changed since it was read.

Troubleshooting

Check these structured log messages:

  • webhook delivered at debug level: the receiver returned 2xx;
  • webhook delivery failed at warning level: network error, timeout, or non-2xx response;
  • webhook notifications dropped at warning level: the subscription queue is full; count is the number dropped during the interval.

If a test endpoint succeeds but no operational event arrives, check the request method, ecosystem, public repository name, enabled, events, OSA Proxy response status, and whether the request was classified as a file or package.

Was this page helpful?