OSPulse

Import a repository

Getting a repository into OSPulse is a three-part job: connect a source control provider, import the repositories you care about from that connection, then wire the provider's webhook so every push keeps the dependency inventory current. This guide walks all three, including the bulk-import pattern for organisations with hundreds of repositories.

What you need

An API token (see Authentication) and admin rights on the GitHub organisation, GitLab group, Azure DevOps organisation, or Bitbucket workspace you are connecting. Everything below runs against https://api.ospulse.app.

Connect a source control provider

A connection is the link between one OSPulse tenant and one upstream organisation. You can hold several — for example a GitHub org and an Azure DevOps org side by side — and repositories are imported from exactly one connection each.

List the connections you already have

Before creating anything, check what is connected. This is also how you get the connectionId values used everywhere else on this page.

bash
curl https://api.ospulse.app/api/v1/source-control/connections \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "connectionId": "6c1e0a2b-9f3d-4a7e-8b21-5d0c4f9ab310",
      "provider": "GitHub",
      "displayName": "acme-engineering",
      "organisationId": "142993",
      "organisationName": "acme-engineering",
      "installationId": "48211037",
      "status": "Active",
      "lastSyncAtUtc": "2026-07-21T09:14:02Z"
    }
  ],
  "total": 1
}

A status other than Active means the upstream credential has expired or the app was uninstalled — reconnect before importing, or the available-repositories call will fail.

Install the GitHub App

GitHub connections are created by installing the OSPulse GitHub App. Start from the Repositories screen in app.ospulse.app — the import panel there both creates connections and imports repositories — which sends you to GitHub to choose the organisation and the repositories the App may see. New tenants are taken through the same step by the onboarding wizard.

GitHub redirects back with an installation_id. If you are driving the flow yourself — for example from an internal onboarding tool — exchange that value for a connection:

bash
curl -X POST https://api.ospulse.app/api/v1/source-control/connections/github/install \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "installationId": "48211037",
        "displayName": "acme-engineering",
        "externalOrganisationName": "acme-engineering"
      }'
json
{
  "connectionId": "6c1e0a2b-9f3d-4a7e-8b21-5d0c4f9ab310",
  "provider": "GitHub",
  "displayName": "acme-engineering",
  "organisationId": "142993",
  "organisationName": "acme-engineering",
  "installationId": "48211037",
  "status": "Active"
}

The call is idempotent on installationId: repeating it with the same installation returns the existing connection rather than creating a duplicate.

GitLab, Azure DevOps and Bitbucket

Those three providers use OAuth or a personal access token rather than an App installation, so their connection is created from the same Repositories screen in the OSPulse UI. Once the connection exists, every step from here on — listing available repositories, importing, webhooks — is identical across all four providers.

List what that connection can see

Ask the connection which repositories are visible to it upstream. This is a live call through to the provider, so it reflects the current permission grant, not a cached copy.

bash
curl "https://api.ospulse.app/api/v1/source-control/connections/6c1e0a2b-9f3d-4a7e-8b21-5d0c4f9ab310/available-repositories" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "externalRepositoryId": "581220934",
      "name": "checkout-api",
      "fullName": "acme-engineering/checkout-api",
      "defaultBranch": "main",
      "visibility": "private",
      "isArchived": false,
      "isFork": false,
      "language": "C#",
      "webUrl": "https://github.com/acme-engineering/checkout-api"
    }
  ],
  "total": 47
}

externalRepositoryId is the provider's own identifier — it is what you pass to the import endpoint, not the name. Names change; identifiers do not.

Import repositories

POST /api/v1/repositories/import takes a connectionId and an array of externalRepositoryIds, and imports them all in one call. It is safe to re-run: repositories already present are reported back rather than duplicated. Two optional fields, workspaceId and teamId, file the repositories under a workspace and owning team as they arrive — see Tenancy.

bash
curl -X POST https://api.ospulse.app/api/v1/repositories/import \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "connectionId": "6c1e0a2b-9f3d-4a7e-8b21-5d0c4f9ab310",
        "externalRepositoryIds": ["581220934", "581220991"]
      }'
json
{
  "requested": 2,
  "imported": 1,
  "alreadyExisted": 1,
  "notFoundUpstream": 0,
  "repositories": [
    {
      "repositoryId": "b28f4d51-77c0-4f2a-9d16-3ea5c7180b44",
      "fullName": "acme-engineering/checkout-api",
      "defaultBranch": "main",
      "wasNewlyImported": true
    },
    {
      "repositoryId": "0f9a1c33-2b64-4d88-a5e7-91cc402fd6b2",
      "fullName": "acme-engineering/checkout-web",
      "defaultBranch": "main",
      "wasNewlyImported": false
    }
  ]
}

Always reconcile requested against imported + alreadyExisted. A non-zero notFoundUpstream means an identifier no longer resolves — usually the repository was deleted, renamed into another organisation, or dropped out of the App's permission grant.

An import queues an initial scan. Poll GET /api/v1/scans?repositoryId=... to watch it, or read Scans for the lifecycle.

Bulk import an entire organisation

For a large organisation, page through available-repositories, filter out the noise (archives and forks rarely deserve a scan budget), and import in batches so a single failure does not cost you the whole run.

bash
#!/usr/bin/env bash
set -euo pipefail
API="https://api.ospulse.app/api/v1"
CONNECTION_ID="6c1e0a2b-9f3d-4a7e-8b21-5d0c4f9ab310"
 
# Every active, non-fork repository the connection can see.
curl -fsS "$API/source-control/connections/$CONNECTION_ID/available-repositories" \
     -H "Authorization: Bearer $OSPULSE_TOKEN" \
| jq -r '.items[] | select(.isArchived == false and .isFork == false) | .externalRepositoryId' \
| xargs -n 25 | while read -r batch; do
    payload=$(jq -n --arg c "$CONNECTION_ID" --args \
              '{connectionId:$c, externalRepositoryIds:$ARGS.positional}' $batch)
 
    curl -fsS -X POST "$API/repositories/import" \
      -H "Authorization: Bearer $OSPULSE_TOKEN" \
      -H "Content-Type: application/json" -d "$payload" \
    | jq -r '"imported=\(.imported) existing=\(.alreadyExisted) missing=\(.notFoundUpstream)"'
 
    sleep 1   # bulk loops are the usual way to meet a rate limit
  done

Watch the RateLimit-* response headers and pause when the remaining budget approaches zero — see Rate limits.

Classify what you imported

An imported repository starts with default metadata. Setting business criticality and lifecycle status is worth doing immediately, because policies, alert routing and exposure prioritisation all key off them — an unclassified repository is treated the same as a retired prototype.

bash
curl -X PATCH https://api.ospulse.app/api/v1/repositories/b28f4d51-77c0-4f2a-9d16-3ea5c7180b44 \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "workspaceId": null,
        "teamId": null,
        "businessCriticality": "Critical",
        "lifecycleStatus": "Active",
        "scanCadenceOverride": "Daily"
      }'
json
{
  "repositoryId": "b28f4d51-77c0-4f2a-9d16-3ea5c7180b44",
  "workspaceId": null,
  "teamId": null,
  "businessCriticality": "Critical",
  "lifecycleStatus": "Active",
  "scanCadenceOverride": "Daily",
  "updatedAtUtc": "2026-07-28T10:02:44Z"
}

The update replaces the whole classification block, so send every field — including the ones you are leaving as null.

To review the estate afterwards, list repositories using the same fields as filters — GET /api/v1/repositories?businessCriticality=Critical&lifecycleStatus=Active. Each item returns provider, visibility, defaultBranch, webUrl, lastScannedAtUtc and dependencyCount alongside the classification you just set, inside the usual items / totalEstimate envelope. workspaceId and teamId are filters here too.

DELETE on the same resource returns 204 and stops future scans, detaching the repository from your inventory. Historical scans and audit entries are retained under your retention settings.

Rescan on push with webhooks

Scheduled scans catch drift eventually. Webhooks catch it on the commit that caused it. OSPulse exposes one inbound ingest endpoint per provider:

ProviderEndpoint
GitHubPOST /api/v1/webhooks/github
GitLabPOST /api/v1/webhooks/gitlab
Azure DevOpsPOST /api/v1/webhooks/azure-devops
BitbucketPOST /api/v1/webhooks/bitbucket

Point the provider at the matching URL, send push events, and set the shared secret held on the connection. For GitHub App connections the webhook is configured by the App installation and needs no manual step. Each delivery is authenticated with the provider's own signature header — an unsigned or mis-signed body is rejected with 401, a body OSPulse cannot parse with 400, and an accepted delivery returns 202 immediately:

json
{
  "deliveryId": "e4a7d0c2-5b18-4f6a-9c31-77aa20ed4b09",
  "provider": "GitHub",
  "blobUri": "deliveries/2026/07/28/e4a7d0c2.json",
  "integrityHash": "sha256:9f2c...",
  "receivedAtUtc": "2026-07-28T10:11:07Z"
}

The 202 confirms receipt, not completion. The push is stored with an integrity hash, matched to an imported repository, and a scan is queued if the push touched a manifest or lock file on a branch OSPulse tracks. Pushes for repositories you have not imported are accepted and discarded — that is expected, not an error.

Retry semantics are the provider's

OSPulse does not retry inbound deliveries. If your provider reports a failed delivery, redeliver it from the provider's own webhook UI, and keep the deliveryId if you need support to trace it.

For outbound webhooks — OSPulse notifying your systems when something changes — see Webhooks and Consume and triage alerts.

Troubleshooting

Fewer repositories listed than you expect. The GitHub App was almost certainly installed with "only select repositories". Change the selection in the GitHub organisation settings; nothing needs re-creating on the OSPulse side.

Private repositories do not appear. The connection's credential must have read access to repository contents and metadata. For GitLab and Bitbucket, a token created by a user who lacks access to the group produces a silently short list rather than an error.

502 or 503 from available-repositories. The upstream provider rejected or timed out. A 409 — there or on import — means the connection itself is unhealthy, typically revoked or mid-reconnection: check status on the connection and reconnect before retrying.

Imported, but zero dependencies. OSPulse reads manifests and lock files from the default branch. If your manifests live on a release branch, or the repository is a monorepo whose manifests sit below the root, the first scan may find nothing. Monorepos are supported — every manifest found in the tree is attributed to the same repository, and manifestPath on each dependency tells you which one it came from. Confirm with GET /api/v1/repositories/{repositoryId}/dependencies.

A repository was renamed upstream. Imports are keyed on the provider identifier, so a rename is picked up on the next sync and does not require re-importing.

Every error body is an RFC 7807 problem document with a correlationId. Quote it when contacting support.

Next steps