Developer guide · REST API v1
Issue and track object ranges from code
For: Business Central developers and DevOps people who want to issue and track object ranges from Business Central, VS Code, scripts or CI.
Reference: the interactive reference is at /docs/api on your BC Object Registry site. The specs are at /api/v1/openapi.json (OpenAPI 3.0) and /api/v1/swagger.json (OpenAPI 2.0, for Power Platform and Copilot Studio).
https://registry.example.com stands for your BC Object Registry address.
Quick start in three calls
Three steps from nothing to an issued range.
In the portal
In the portal, open API keys → Create API key. Name it after where it will be used (for example "BC sandbox" or "GitHub Actions"), and choose its scopes. The key is shown once: copy it into a secret store.
GET /api/v1/me
curl -H "X-API-Key: $BCOR_API_KEY" https://registry.example.com/api/v1/me
POST /api/v1/ranges/allocate
curl -H "X-API-Key: $BCOR_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-X POST https://registry.example.com/api/v1/ranges/allocate \
-d '{"name":"Contoso Labels","size":20,"environments":["Production"]}'
The response has the range, and idRanges ready for app.json:
{
"range": { "from": 50120, "to": 50139 },
"rangeText": "50120–50139",
"idRanges": [{ "from": 50120, "to": 50139 }],
"app": { "id": "…", "name": "Contoso Labels", "status": "Pending", "environments": ["Production"] }
}
Reference. The interactive reference is at /docs/api on your BC Object Registry site. The specs are at /api/v1/openapi.json (OpenAPI 3.0) and /api/v1/swagger.json (OpenAPI 2.0, for Power Platform and Copilot Studio).
X-API-Key: <key>, or as Authorization: Bearer <key>. Keys look like bcor_ab12cd34_….| Scope | Allows |
|---|---|
| read | List apps, tenants and pools; free ranges; check a range; /me |
| allocate | POST /ranges/allocate (issue a new range) |
| write | Register, update and delete apps; import; add and update tenants |
Members create and revoke their own keys; org admins see and revoke everyone's. Plans allow these active keys:
Revoking a key takes effect at once. Keys are stored only as a hash; nobody can show you a key again, including us.
Object IDs only collide inside one Business Central tenant, so every PTE app belongs to a tenant.
For partners and everyone else
Have one tenant: you can leave the tenant out.
Has a registry per customer tenant: name it with tenantId (our id, from GET /tenants) or bcTenantId (the Microsoft Entra tenant ID). From AL, bcTenantId is AzureADTenant.GetAadTenantId(), so code running inside a customer's Business Central lands in that customer's registry.
Are partner-wide: send "type": "AppSource" and no tenant.
Are named by their Business Central names (case-insensitive), e.g. ["Production", "UAT"]. Unknown names are refused with the tenant's list. Ranges are always checked across the whole tenant, whatever the environment.
All paths are under /api/v1.
GET POST PATCH DELETE
read allocate write
| Method and path | Scope | What it does |
|---|---|---|
| GET/me | read | The workspace, plan and scopes this key acts with |
| Tenants | ||
| GET/tenants | read | Tenants with their environments and admin center links |
| GET/tenants/{id} | read | One tenant, by our id or its BC Tenant ID |
| POST/tenants | write | Add a customer tenant (Premium (Partner)) |
| PATCH/tenants/{id} | write | Rename, change domain or Tenant ID, activate or deactivate |
| Apps | ||
| GET/apps | read | List apps. Filters: tenantId, bcTenantId, type, status, appId, q |
| GET/apps/{id} | read | One app with ranges, environments and overlaps |
| POST/apps | write | Register an app that already has its range(s) |
| PATCH/apps/{id} | write | Change only the fields sent (e.g. set appId and status: "Active") |
| DELETE/apps/{id}?confirm=<name> | write | Delete an app and free its ranges; restorable in the portal for 30 days |
| Ranges and pools | ||
| POST/ranges/allocate | allocate | Issue the next free range (first fit, safe under concurrency) |
| POST/ranges/check | read | Is from–to free? Which apps would it overlap? |
| GET/pools | read | Pools with usage |
| GET/pools/{id}/free | read | A pool's free ranges, live |
| Import | ||
| POST/import | write | Bulk import existing apps, with a dry-run preview |
| GET/import/{id} | write | An import's status and counts |
| POST/import/{id}/undo | write | Undo a committed import within 24 hours |
The next pages show the request bodies for issuing a range, checking a range, registering an app from CI, and importing.
POST /ranges/allocateallocate
{ "name": "Contoso Labels", "size": 20, "bcTenantId": "…", "environments": ["Production"] }
type (PTE default, or AppSource), tenantId, poolId (a specific pool, e.g. one of several AppSource pools).PATCH /apps/{id}.POST /ranges/checkread
{ "from": 50100, "to": 50119 }
Returns free (true/false), the apps it would overlap, and the pool it falls in.
POST /appswrite
{
"name": "Warehouse Labels",
"appId": "d1000001-0000-4000-8000-000000000001",
"ranges": [{ "from": 50100, "to": 50119 }],
"status": "Active",
"repoName": "warehouse-labels",
"environments": ["Production"]
}
ranges can also be a string: "50100-50119; 50200-50209". Overlaps are allowed, flagged and returned in warnings. A duplicate name or App ID is a 409.
POST /import takes either apps (a JSON array shaped like POST /apps) or file ({ "name": "registry.json", "content": "…" }). Files can be your GitHub registry's registry.json or ObjectRegistry.md, an app.json, a CSV, or a .zip of repos sent with "encoding": "base64".
1 · Preview
The default,dryRun: true: returns each row as new, duplicate, invalid or over-limit, with overlaps and warnings, plus a previewId. Nothing is written.2 · Commit
Applies exactly what was previewed. Duplicates are skipped unlessdecisions says update or new (by row index).3 · Undo
POST /import/{previewId}/undo within 24 hours.Commit body
{ "dryRun": false, "previewId": "…", "decisions": { "3": "update" } }
Errors use RFC 9457 problem details, Content-Type: application/problem+json:
{ "type": "about:blank", "title": "Tenant required", "status": 422, "detail": "Name the tenant with tenantId or bcTenantId…", "tenants": [ … ] }
| Status | Meaning |
|---|---|
| 400 | Malformed JSON |
| 401 | Missing, unknown or revoked key; disabled workspace; blocked key owner |
| 403 | The key lacks the scope |
| 404 | Not found (or not in this workspace) |
| 409 | Conflict: a duplicate name or App ID |
| 413 | File too large (4 MB) |
| 422 | Refused: validation, plan limit, inactive tenant, no free block, unknown environment |
| 429 | Rate limit exceeded |
Validation errors include errors: [{ "path": "size", "message": "…" }].
Per key, per minute.
| Header | Carried on |
|---|---|
| X-RateLimit-Limit | Every response |
| X-RateLimit-Remaining | Every response |
| X-RateLimit-Reset | Every response (Unix seconds) |
| Retry-After | A 429 (seconds) |
Send Idempotency-Key: <unique string> (a UUID is ideal) on POST /ranges/allocate, POST /apps and POST /import commits. If a call times out and you retry with the same key, you get the first response back (with Idempotent-Replayed: true) instead of a second range.
The call times out.
Use the same Idempotency-Key.
You get the first response, not a second range.
Keys are remembered for 24 hours per workspace.
The samples are in the repository under docs/api/samples/. Full listings are in the appendix.
Issues a range for the tenant and environment it runs in, and checks ranges.
GetAadTenantId() as bcTenantId and the current environment name.SecretText headers need Business Central 2024 release wave 1 (v24) or later.var
Registry: Codeunit "OBCF Object Registry Client";
begin
Registry.SetApiKey(ApiKeyFromSetupPage);
Message('Issued %1', Registry.IssueRange('Warehouse Labels', 20));
end;
Issues a range and writes idRanges into app.json:
$env:BCOR_API_KEY = "bcor_…"
./issue-range.ps1 -Name "Contoso Labels" -Size 20 -Environments Production
Adds Run Task → Issue object range. It prompts for a name and size and runs the PowerShell script on the open project.
Registers the repository's app.json on every push to main. It uses POST /import, so apps already registered are skipped. Store the key as the BCOR_API_KEY secret (write scope).
Covers /me, listing, issuing, checking and importing a registry.json.
Use the OpenAPI 2.0 file (/api/v1/swagger.json) as a Copilot Studio REST API tool or a Power Platform custom connector. See the Copilot Studio connector guide.
The real samples from docs/api/samples/, in full.
ALObjectRegistryClient.Codeunit.alBusiness Central client codeunit · lines 1–52
/// <summary>/// Calls the BC Object Registry API from Business Central (AL HttpClient)./// Issues a new object range for this tenant and environment, or checks one./// The API key lives in Isolated Storage (company-independent), never in code./// Pick an object ID from your own range; 50100 is only an example./// </summary>codeunit 50100 "OBCF Object Registry Client"{ Access = Public; var BaseUrlTxt: Label 'https://registry.onlybcfans.com/api/v1', Locked = true; // replace with your BC Object Registry address ApiKeyStorageKeyTok: Label 'OBCF-ObjectRegistry-ApiKey', Locked = true; procedure SetApiKey(ApiKey: SecretText) begin IsolatedStorage.Set(ApiKeyStorageKeyTok, ApiKey, DataScope::Module); end; /// <summary>Issue the next free range of Size IDs for ProjectName in this tenant, marked as deployed to this environment.</summary> /// <returns>The range as text, e.g. "50120–50139".</returns> procedure IssueRange(ProjectName: Text; Size: Integer): Text var AzureADTenant: Codeunit "Azure AD Tenant"; EnvironmentInformation: Codeunit "Environment Information"; Body: JsonObject; Environments: JsonArray; Response: JsonObject; Token: JsonToken; begin Body.Add('name', ProjectName); Body.Add('size', Size); // Lands in the registry for this customer's tenant (Premium (Partner) has one per tenant). Body.Add('bcTenantId', AzureADTenant.GetAadTenantId()); Environments.Add(EnvironmentInformation.GetEnvironmentName()); Body.Add('environments', Environments); Response := Send('POST', '/ranges/allocate', Body, CreateGuid()); Response.Get('rangeText', Token); exit(Token.AsValue().AsText()); end; /// <summary>True when FromId..ToId is free in this tenant's registry.</summary> procedure IsRangeFree(FromId: Integer; ToId: Integer): Boolean var AzureADTenant: Codeunit "Azure AD Tenant"; Body: JsonObject; Response: JsonObject; Token: JsonToken; begin Body.Add('from', FromId); Body.Add('to', ToId);
Continued on the next page.
ALObjectRegistryClient.Codeunit.alcontinued · lines 53–95
Body.Add('bcTenantId', AzureADTenant.GetAadTenantId()); Response := Send('POST', '/ranges/check', Body, CreateGuid()); Response.Get('free', Token); exit(Token.AsValue().AsBoolean()); end; local procedure Send(Method: Text; Path: Text; Body: JsonObject; IdempotencyKey: Guid) Result: JsonObject var Client: HttpClient; Request: HttpRequestMessage; Response: HttpResponseMessage; Content: HttpContent; Headers: HttpHeaders; ApiKey: SecretText; BodyText: Text; ResponseText: Text; ProblemErr: Label 'BC Object Registry returned %1: %2', Comment = '%1 = HTTP status, %2 = problem detail'; NoKeyErr: Label 'Set the BC Object Registry API key first (SetApiKey).'; begin if not IsolatedStorage.Get(ApiKeyStorageKeyTok, DataScope::Module, ApiKey) then Error(NoKeyErr); Body.WriteTo(BodyText); Content.WriteFrom(BodyText); Content.GetHeaders(Headers); Headers.Remove('Content-Type'); Headers.Add('Content-Type', 'application/json'); Request.Method := Method; Request.SetRequestUri(BaseUrlTxt + Path); Request.Content := Content; Request.GetHeaders(Headers); Headers.Add('X-API-Key', ApiKey); // A retry with the same key returns the same range instead of issuing a second one. Headers.Add('Idempotency-Key', Format(IdempotencyKey, 0, 4)); Client.Send(Request, Response); Response.Content.ReadAs(ResponseText); if not Response.IsSuccessStatusCode() then Error(ProblemErr, Response.HttpStatusCode(), ResponseText); Result.ReadFrom(ResponseText); end;}
Questions or problems? Email info@onlybcfans.com.
BC Object Registry by OnlyBCFans. Not affiliated with Microsoft. Microsoft, Dynamics 365 and Business Central are trademarks of the Microsoft group of companies.
PowerShellissue-range.ps1Issue a range and write idRanges into app.json
# Issue a new object range with PowerShell 7 and write it into app.json.# Usage: ./issue-range.ps1 -Name "Contoso Labels" -Size 20 [-BcTenantId <guid>] [-Environments Production,UAT]param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [int] $Size, [string] $BcTenantId, [string[]] $Environments = @(), [string] $AppJson = "./app.json", [string] $BaseUrl = $env:BCOR_URL ?? "https://registry.onlybcfans.com")$key = $env:BCOR_API_KEYif (-not $key) { throw "Set BCOR_API_KEY to an API key from the portal (API keys)." } $body = @{ name = $Name; size = $Size; environments = $Environments }if ($BcTenantId) { $body.bcTenantId = $BcTenantId } $result = Invoke-RestMethod -Method Post -Uri "$BaseUrl/api/v1/ranges/allocate" ` -Headers @{ "X-API-Key" = $key; "Idempotency-Key" = [guid]::NewGuid().ToString() } ` -ContentType "application/json" -Body ($body | ConvertTo-Json) Write-Host "Issued $($result.rangeText) for $Name"if (Test-Path $AppJson) { $app = Get-Content $AppJson -Raw | ConvertFrom-Json $app | Add-Member -NotePropertyName idRanges -NotePropertyValue $result.idRanges -Force $app | ConvertTo-Json -Depth 20 | Set-Content $AppJson Write-Host "Updated idRanges in $AppJson"}
VS Codevscode-tasks.jsonRun Task → Issue object range
// .vscode/tasks.json: "Run Task → Issue object range" asks for a name and size, then writes idRanges into app.json.// Needs PowerShell 7 and issue-range.ps1 next to app.json, with BCOR_API_KEY set in your environment.{ "version": "2.0.0", "inputs": [ { "id": "name", "type": "promptString", "description": "Project name" }, { "id": "size", "type": "promptString", "description": "How many object IDs?", "default": "20" } ], "tasks": [ { "label": "Issue object range", "type": "shell", "command": "pwsh", "args": ["-File", "${workspaceFolder}/issue-range.ps1", "-Name", "${input:name}", "-Size", "${input:size}", "-AppJson", "${workspaceFolder}/app.json"], "problemMatcher": [] } ]}
curlcurl.sh/me, listing, issuing, checking, importing
#!/usr/bin/env bash# curl examples. Set BCOR_URL (e.g. https://registry.onlybcfans.com) and BCOR_API_KEY first.set -euo pipefailH=(-H "X-API-Key: ${BCOR_API_KEY}" -H "Content-Type: application/json") # Who am I (workspace, plan, scopes)curl -s "${H[@]}" "$BCOR_URL/api/v1/me" # List active PTE apps in one tenant (by BC Tenant ID)curl -s "${H[@]}" "$BCOR_URL/api/v1/apps?type=PTE&status=Active&bcTenantId=00000000-0000-0000-0000-000000000000" # Issue 20 IDs, marked as deployed to Production; safe to retry with the same Idempotency-Keycurl -s "${H[@]}" -H "Idempotency-Key: $(uuidgen)" -X POST "$BCOR_URL/api/v1/ranges/allocate" \ -d '{"name":"Contoso Labels","size":20,"environments":["Production"]}' # Is 50100–50119 free?curl -s "${H[@]}" -X POST "$BCOR_URL/api/v1/ranges/check" -d '{"from":50100,"to":50119}' # Import your GitHub registry.json (preview first, then commit with the previewId)curl -s "${H[@]}" -X POST "$BCOR_URL/api/v1/import" \ -d "$(jq -n --arg c "$(cat registry.json)" '{dryRun:true, file:{name:"registry.json", content:$c}}')"
GitHub Actionsgithub-actions-register-app.ymlRegister app.json on push to main
# .github/workflows/register-app.yml# On every push to main, register (or update) this repo's app.json in BC Object Registry.# Secrets: BCOR_API_KEY (write scope). Variables: BCOR_URL, BCOR_BC_TENANT_ID (Partner workspaces).name: Register app in BC Object Registryon: push: branches: [main] paths: [app.json]jobs: register: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Register or update the app env: BCOR_URL: ${{ vars.BCOR_URL }} BCOR_API_KEY: ${{ secrets.BCOR_API_KEY }} BCOR_BC_TENANT_ID: ${{ vars.BCOR_BC_TENANT_ID }} run: | set -euo pipefail content=$(jq -c '.' app.json) body=$(jq -n --arg c "$content" --arg t "${BCOR_BC_TENANT_ID:-}" --arg repo "${GITHUB_REPOSITORY#*/}" \ '{dryRun:false, file:{name:"app.json", content:$c}} + (if $t != "" then {bcTenantId:$t} else {} end)') # Import keeps existing apps (duplicates are skipped by default); new apps are registered. curl -sf -H "X-API-Key: $BCOR_API_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: $GITHUB_SHA" \ -X POST "$BCOR_URL/api/v1/import" -d "$body" | jq '.counts, .committed'