Skip to content

API Guide

The de.NBI Service Registration Platform REST API allows programmatic access to service registrations.

Interactive documentation is available at:

  • Swagger UI: /api/docs/
  • ReDoc: /api/redoc/
  • OpenAPI schema (JSON): /api/schema/

Both Swagger UI (swagger-ui-dist 5.18.2) and ReDoc (2.2.0) assets are vendored locally in static/ — no CDN or external requests are made when loading the docs pages.


Authentication

Two authentication schemes are supported.

Admin API Key (scoped, independent of user accounts)

Authorization: AdminKey <key>

Machine-to-machine keys that are independent of any staff user account and carry an explicit scope. Create as many as needed — one per consumer, rotate individually.

Scope HTTP methods allowed Typical use case
read GET / HEAD / OPTIONS only Public-facing website, dashboard, read-only CI
full All methods (GET/POST/PATCH/DELETE) Trusted back-end integration

Why use a read scope key?

If you give a read key to a third-party website that renders registry data, leaking the key is low-risk: the worst-case outcome is that someone reads data that might already be public. They cannot modify submissions, create or delete reference data, or access any field that the serialisers exclude from responses (submission_ip, etc.).

Creating a key (see Admin Guide → Admin API Keys):

  1. Log in to /admin-denbi/
  2. Go to API → Admin API Keys → Add Admin API Key
  3. Enter a label (e.g. Public website) and choose the scope
  4. Save — the full plaintext key appears once. Copy it now.

Using it:

# Read-only access — safe to embed in a public application
curl https://service-registry.bi.denbi.de/api/v1/submissions/ \
  -H "Authorization: AdminKey <your-key>"

# Attempt to write with a read-scope key → 403 Forbidden
curl -X POST https://service-registry.bi.denbi.de/api/v1/categories/ \
  -H "Authorization: AdminKey <read-key>" \
  -H "Content-Type: application/json" \
  -d '{"name": "New"}'
# → {"detail": "This key is read-only. Use a full-access Admin API Key to modify data."}

Revoking a key:

Set Is active to unchecked in the admin UI. The record is retained for audit purposes; the key immediately stops authenticating.


Submission API Key

Authorization: ApiKey <key>

Issued when a service is registered (via the web form or POST /api/v1/submissions/). Scoped to a single submission. The plaintext key is shown once — store it securely.

Two scopes are available (set by admins via the API Key admin):

Scope REST API Web edit form (/update/edit/<submission_id>/)
read GET only View form (GET), cannot submit
write GET + PATCH View and submit changes

Scope is enforced consistently in both the REST API and the web edit form.

Note: The scope field on SubmissionAPIKey defaults to write. Read-only keys can only be set via the standalone Submission API Keys list view in the admin. A read key stored in the update session can load the pre-populated form for inspection but any POST attempt is rejected and redirected to the key-entry page.

Using it:

curl https://service-registry.bi.denbi.de/api/v1/submissions/<id>/ \
  -H "Authorization: ApiKey <your-key>"

Endpoints

Register a service

POST /api/v1/submissions/ — no authentication required. Submits a new service registration.

Response (201):

{
  "id": "26a59fcb-...",
  "service_name": "MyTool",
  "api_key": "oGzQk9...",
  "api_key_warning": "This key is shown ONCE. Store it securely.",
  "status": "submitted",
  ...
}

List all submissions

GET /api/v1/submissions/ — requires admin API Key. Returns paginated full detail for all submissions.

Query parameters:

Parameter Example Description
status ?status=approved Filter by status (draft, submitted, under_review, approved, rejected, deprecated)
service_center ?service_center=BioinfoProt Filter by centre short name
year_established ?year_established=2021 Filter by year
register_as_elixir ?register_as_elixir=true Filter by ELIXIR flag
ordering ?ordering=-submitted_at Sort (prefix - for descending)

Response (200):

{
  "count": 42,
  "next": "http://.../api/v1/submissions/?page=2",
  "previous": null,
  "results": [
    {
      "id": "...",
      "service_name": "...",
      "status": "approved",
      "edam_topics": [{"uri": "...", "accession": "topic_0091", "label": "Proteomics"}],
      "edam_operations": [...],
      "responsible_pis": [{"last_name": "...", "orcid": "..."}],
      "biotoolsrecord": {
        "id": "...",
        "biotools_id": "my-tool",
        "biotools_url": "https://bio.tools/my-tool",
        "name": "My Tool",
        "description": "A tool for ...",
        "homepage": "https://example.com/my-tool",
        "version": ["1.0", "1.1"],
        "license": "MIT",
        "maturity": "Mature",
        "cost": "Free of charge",
        "tool_type": ["Web application", "Command-line tool"],
        "operating_system": ["Linux", "Mac"],
        "edam_topic_uris": ["http://edamontology.org/topic_0091"],
        "edam_topics_resolved": [
          {"uri": "http://edamontology.org/topic_0091", "accession": "topic_0091", "label": "Proteomics"}
        ],
        "functions": [
          {
            "position": 0,
            "operations": [{"uri": "http://edamontology.org/operation_0004", "term": "Operation"}],
            "inputs": [{"data": {"uri": "...", "term": "Sequence"}, "formats": []}],
            "outputs": [{"data": {"uri": "...", "term": "Report"}, "formats": []}],
            "cmd": null,
            "note": null
          }
        ],
        "publications": [{"doi": "10.1000/xyz", "pmid": null, "pmcid": null, "type": "Primary", "note": null}],
        "documentation": [{"url": "https://example.com/docs", "type": "General", "note": null}],
        "download": [],
        "links": [],
        "last_synced_at": "2024-03-15T10:30:00Z",
        "sync_error": null
      },
      ...
    }
  ]
}

Retrieve a submission

GET /api/v1/submissions/{id}/ — requires ApiKey. Returns your own submission in full detail. Returns 403 if the key does not belong to this submission.

Description fields. service_description is the raw text as submitted (only line endings, Unicode normalisation and leading/trailing whitespace are normalised): plain text that may contain Markdown and any characters, including <, > and & and text that looks like HTML tags (rows saved by older versions of the web form may hold HTML entities such as &gt; instead). It is not HTML: HTML-escape it wherever you insert it into a page, or use service_description_html, which is always safe to insert. The detail response (this endpoint, and the create and update responses) also includes a read-only service_description_html:

  • It is always safe HTML, ready to insert into a page.
  • When Markdown rendering is enabled on the server, it is the sanitized rendered HTML (CommonMark): paragraphs, line breaks, bold/italic, lists (an ordered list may carry a numeric start attribute), blockquotes, headings and links. Headings are shifted down three levels, so it may contain h4, h5 and h6 (# becomes h4, ## h5, ### and deeper h6) but never h1 to h3. Only absolute http:// or https:// links with a host and non-empty mailto: links are kept; other links (including relative ones and host-less ones such as https:example.org) are reduced to their text, and so are links with no visible text. Kept links carry rel="nofollow noopener noreferrer"; http/https links also get target="_blank", mailto links do not.
  • When it is disabled, it is the plain text HTML-escaped once, with no paragraph or line-break markup.

Descriptions saved before Markdown support was released may contain HTML entities such as &gt; in the raw service_description (the old web form escaped its input); newer rows are raw. service_description_html decodes those entities in both cases, so a legacy x &gt; 5 displays as x > 5, never as a literal &gt;: with Markdown enabled it is rendered as above, and with Markdown disabled the decoded plain text is escaped exactly once. Consumers that display descriptions should prefer service_description_html over the raw field.

service_description_html is not included in the list endpoint (GET /api/v1/submissions/) and is ignored if sent in a POST or PATCH. Length validation applies to the raw service_description.

{
  "service_description": "Aligns **short reads**.\n\n- fast\n- accurate",
  "service_description_html": "<p>Aligns <strong>short reads</strong>.</p>\n<ul>\n<li>fast</li>\n<li>accurate</li>\n</ul>"
}

Update a submission

PATCH /api/v1/submissions/{id}/ — requires ApiKey with write scope. Partial update — include only changed fields.

Updating an approved submission resets its status to submitted for re-review when a field that is not listed in no_reset_fields actually changes (configured in site.toml [submission]), the same rule as the web edit form. Sending a field with the value it already has is not a change, so a GET, modify, PATCH round-trip that echoes unchanged fields keeps the service approved; values are compared as stored, so differences only in surrounding whitespace or Unicode normalisation (NFC) do not count either. If no no_reset_fields are configured, any actual change resets the status. The default exempt set is the list of external links, EDAM annotations, keywords, publications, contact fields, KPI fields, and comments — see no_reset_fields in site.toml for the authoritative list. Changing only exempt fields on an approved submission preserves its status and maturity tags. The content change and the reset are saved in one transaction.

When a reset does occur, primary_maturity_tag and secondary_maturity_tags are also cleared (they are only valid on approved services). The submitter update email includes a lifecycle notice.

curl -X PATCH https://service-registry.bi.denbi.de/api/v1/submissions/<id>/ \
  -H "Authorization: ApiKey <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"kpi_start_year": "2026"}'

Full PUT is not supported — use PATCH.

The service name is immutable after submission

service_name is read-only on update: it can be set at registration (POST) but never changed via PATCH. A PATCH that includes a different service_name still succeeds (200) — the name is left unchanged, all other fields are applied, and the response gains a warnings array noting the rename was not applied. Sending the same name (in any whitespace or Unicode form) is silently accepted with no warning. Only a de.NBI administrator can correct a submitted name.

{
  "id": "…",
  "service_name": "Original Name",
  "warnings": [
    "The service name cannot be changed after a service is submitted. Your requested name change was not applied; all other changes were accepted."
  ]
}

Email notifications on PATCH

Every successful PATCH triggers the same notification flow as a submitter web-form edit: an admin email with the full submission report, a field-level what changed diff table, and a direct link to the admin change view. If any fields actually changed, the submitter also receives a separate confirmation email with the same diff table. If the edit resets the status (non-exempt field change on an approved service), the submitter email includes a lifecycle notice explaining the reset. When the request contains no actual changes, the submitter gets no email and the admin email has no diff table (as for a web-form save without changes).


Custom permissions

Two semantic permissions are defined on ServiceSubmission beyond the standard CRUD permissions:

Codename What it gates API endpoint impact
submissions.approve_servicesubmission Approve and reject status transitions; bulk approve/reject actions. Admin-only via /admin/. Not exposed via API.
submissions.manage_apikeys Issue, reset, and revoke SubmissionAPIKey objects. Admin-only via /admin/. Not exposed via API.

These permissions are enforced solely in the Django admin backend. See Admin Guide → Custom permission codenames for more detail.


Service logos can be attached to a submission at registration time or added/replaced later via PATCH.

On POST (new registration):

curl -X POST https://service-registry.bi.denbi.de/api/v1/submissions/ \
  -H "Authorization: ApiKey <your-key>" \
  -F "service_name=MyTool" \
  -F "logo=@service-logo.png"

On PATCH (update existing submission):

curl -X PATCH https://service-registry.bi.denbi.de/api/v1/submissions/<id>/ \
  -H "Authorization: ApiKey <your-key>" \
  -F "logo=@service-logo.svg"

Use multipart/form-data for logo uploads

When uploading a logo, send the request as multipart/form-data (-F flags in curl) instead of application/json. You can mix file and text fields in the same request. JSON-only requests (Content-Type: application/json) cannot carry file data.

Logo field behaviour:

Field Direction Type Notes
logo write-only file Accepted in multipart/form-data requests only, on create (POST) and update (PATCH). Omit to leave the existing logo unchanged.
logo_url read-only string | null Absolute URL to the stored logo file, or null if no logo has been uploaded. Returned in all submission responses.

Accepted formats and default limits (configurable in config/site.toml [uploads]; the logo field description in the OpenAPI schema shows the configured values):

Format Limit
PNG / JPEG logo_max_bytes (10 MB) and logo_max_pixels (25 megapixels)
SVG logo_max_svg_bytes (1 MB)

A file over these limits, of an unsupported type, or one that cannot be processed is rejected with 400 Bad Request and a validation message for the logo field; nothing is stored. SVG content outside the rules below is removed and the rest of the file is stored.

Processing applied automatically:

  • Magic-byte type detection (file extension and MIME header are never trusted)
  • JPEG/PNG: pixel count read from the header before decoding, then re-encoded via Pillow to strip EXIF metadata and verify integrity
  • SVG: parsed with Python's stdlib XML parser (external entities are not loaded, and text from internal entities goes through the same rules as the rest of the file); the root must be <svg>; only standard drawing elements are kept (shapes, text, gradients, patterns, clip paths, masks, filters, styles); an SVG containing a bitmap image (<image>) or other content from outside the file is rejected; on* attributes and namespaced attributes other than xlink:href, xml:space and xml:lang are removed; links may only point inside the logo (#id); CSS references may also use embedded data: resources such as fonts
  • Original filename is discarded; the file is stored under a UUID path (media/logos/<uuid4>.<ext>)

Old logos are not deleted when a logo is replaced — previous files remain on disk.


Maturity Tags

Approved services can be tagged with a primary maturity stage (Mature, Emerging, Legacy) and optional secondary tags (Unstable, etc.). Tags provide lifecycle metadata for users browsing the registry.

Response fields (read-only):

  • primary_maturity_tag: "mature" | "emerging" | "legacy" | null
  • secondary_maturity_tags: array of strings (e.g., ["unstable"] or [])

Admin-only — read-only in the API

Maturity tags are assigned exclusively by admins via the Django admin backend. Any primary_maturity_tag or secondary_maturity_tags values included in a POST or PATCH request body are silently ignored. Tags appear in GET responses so consumers can read the admin-assigned lifecycle stage.

Filter by maturity:

# List all approved Mature services
curl https://service-registry.bi.denbi.de/api/v1/submissions/?primary_maturity_tag=mature \
  -H "Authorization: AdminKey <your-key>"

# List all services tagged Unstable
curl https://service-registry.bi.denbi.de/api/v1/submissions/?secondary_maturity_tags=unstable \
  -H "Authorization: AdminKey <your-key>"

Tags are cleared on unapproval

When an admin changes a service's status away from Approved (via the admin backend), all maturity tags are automatically cleared. The API will return null / [] for the tag fields on such services. Tags can be reassigned after the service is re-approved.


Reference data

All reference data endpoints require an admin API Key. All three resources support full CRUD and follow the same pattern.

Method URL pattern Description
GET /api/v1/categories/ List all categories (active + inactive)
POST /api/v1/categories/ Create a category
GET /api/v1/categories/{id}/ Retrieve a category
PATCH /api/v1/categories/{id}/ Partial update
PUT /api/v1/categories/{id}/ Full update
DELETE /api/v1/categories/{id}/ Soft-delete (sets is_active=False)

Same pattern applies for /api/v1/service-centers/{id}/ and /api/v1/pis/{id}/.

DELETE is a soft-delete — the record is retained in the database and remains linked to existing submissions. is_active=False hides it from the registration form but does not break any foreign keys.

Filter: ?is_active=true|false narrows the list to active or inactive records. Without the filter, both active and inactive records are returned.

Fields — /api/v1/categories/

Field Type Writable Notes
id integer no Auto-assigned
name string yes Must be unique
is_active boolean yes Defaults to true

Fields — /api/v1/service-centers/

Field Type Writable Notes
id UUID no Auto-assigned
short_name string yes e.g. "HD-HuB"
full_name string yes Full official name
website URL yes Optional
is_active boolean yes Defaults to true

Fields — /api/v1/pis/

Field Type Writable Notes
id UUID no Auto-assigned
last_name string yes Required
first_name string yes Required
display_name string no Computed ("Last, First")
email string yes Internal — never exposed in submission responses
institute string yes Optional
orcid string yes Optional; validated format + checksum
is_active boolean yes Defaults to true
is_associated_partner boolean yes Mark true for the generic "Associated partner" entry

curl examples

# List all service centres (active + inactive)
curl https://service-registry.bi.denbi.de/api/v1/service-centers/ \
  -H "Authorization: ApiKey <admin-key>"

# List active categories only
curl "https://service-registry.bi.denbi.de/api/v1/categories/?is_active=true" \
  -H "Authorization: ApiKey <admin-key>"

# Create a new PI
curl -X POST https://service-registry.bi.denbi.de/api/v1/pis/ \
  -H "Authorization: ApiKey <admin-key>" \
  -H "Content-Type: application/json" \
  -d '{"last_name": "Smith", "first_name": "Alice", "email": "a.smith@example.com", "institute": "Example University"}'

# Deactivate a service centre (soft-delete)
curl -X DELETE https://service-registry.bi.denbi.de/api/v1/service-centers/<id>/ \
  -H "Authorization: ApiKey <admin-key>"

# Re-activate a category via PATCH
curl -X PATCH https://service-registry.bi.denbi.de/api/v1/categories/<id>/ \
  -H "Authorization: ApiKey <admin-key>" \
  -H "Content-Type: application/json" \
  -d '{"is_active": true}'

bio.tools records

Bio.tools metadata is automatically fetched and kept in sync when a submission includes a biotools_id. The data is embedded directly in every submission response under the biotoolsrecord key (see above).

A standalone read-only endpoint is also available for admin API keys:

Method URL Description
GET /api/v1/biotools/ List all bio.tools records
GET /api/v1/biotools/{biotools_id}/ Retrieve one bio.tools record by its bio.tools ID

Authentication: admin API Key required.

Both endpoints return the same field set as the biotoolsrecord object shown in the submission response above, plus a submission link field pointing to the associated submission.

# List all synced bio.tools records
curl https://service-registry.bi.denbi.de/api/v1/biotools/ \
  -H "Authorization: AdminKey <admin-key>"

# Retrieve a specific record
curl https://service-registry.bi.denbi.de/api/v1/biotools/my-tool/ \
  -H "Authorization: AdminKey <admin-key>"

bio.tools sync actions

Admins can trigger a manual re-sync from the bio.tools record list in the Django admin (Bio.tools records → select records → "Sync selected records from bio.tools now"). Syncs are queued as Celery tasks and run in the background.


EDAM ontology terms

GET /api/v1/edam/ — public, no authentication required. Returns all non-obsolete EDAM terms.

Filter: ?branch=topic|operation|data|format, ?q=<search>

EDAM term detail

GET /api/v1/edam/{accession}/ — public. Look up by accession (e.g. topic_0091) or UUID.


Response shape

All error responses follow this envelope:

{
  "error": { "detail": "Authentication credentials were not provided." },
  "request_id": "8e26f6d5-0094-48ea-9a36-c417921815a9"
}

The request_id is included in every response (success and error) as X-Request-ID header and in error bodies. Use it when reporting issues.


Field visibility

Field POST/PATCH GET response Notes
internal_contact_name required never Write-only; stored for admin use only
internal_contact_email required never Write-only; stored for admin use only
primary_maturity_tag ignored yes Read-only in API; set by admins via backend. See Maturity Tags
secondary_maturity_tags ignored yes Read-only in API; set by admins via backend. See Maturity Tags
service_description_html ignored detail only Read-only; safe HTML rendering of service_description. See Retrieve a submission
submission_ip — never Server-generated; not exposed via API
user_agent_hash — never Server-generated; not exposed via API

curl examples

# Register a new service (public) — internal_contact_* are required
curl -X POST https://service-registry.bi.denbi.de/api/v1/submissions/ \
  -H "Content-Type: application/json" \
  -d @submission.json
# submission.json must include internal_contact_name and internal_contact_email

# List all submissions (admin key)
curl https://service-registry.bi.denbi.de/api/v1/submissions/ \
  -H "Authorization: AdminKey <admin-key>"

# Retrieve your submission (ApiKey)
curl https://service-registry.bi.denbi.de/api/v1/submissions/<id>/ \
  -H "Authorization: ApiKey <your-key>"

# Update a field (ApiKey, write scope)
curl -X PATCH https://service-registry.bi.denbi.de/api/v1/submissions/<id>/ \
  -H "Authorization: ApiKey <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"website_url": "https://new-url.example.com"}'

# Upload or replace a logo (ApiKey, write scope — multipart/form-data)
curl -X PATCH https://service-registry.bi.denbi.de/api/v1/submissions/<id>/ \
  -H "Authorization: ApiKey <your-key>" \
  -F "logo=@service-logo.png"

# Browse EDAM topics (public)
curl "https://service-registry.bi.denbi.de/api/v1/edam/?branch=topic&q=proteomics"