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.
curl https://api.ospulse.app/api/v1/source-control/connections \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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:
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"
}'{
"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.
curl "https://api.ospulse.app/api/v1/source-control/connections/6c1e0a2b-9f3d-4a7e-8b21-5d0c4f9ab310/available-repositories" \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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.
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"]
}'{
"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.
#!/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
doneWatch 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.
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"
}'{
"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:
| Provider | Endpoint |
|---|---|
| GitHub | POST /api/v1/webhooks/github |
| GitLab | POST /api/v1/webhooks/gitlab |
| Azure DevOps | POST /api/v1/webhooks/azure-devops |
| Bitbucket | POST /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:
{
"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.
