Skip to content

B2INST v3 (InvenioRDM 13) API Submission Guide#

This guide follows the soil-moisture sensor examples from the release and migration correspondence, with request bodies and workflows checked against the B2INST deployment code. It covers metadata-only records, optional files, community publication, and updates to published records.

Before you start#

Create a personal access token under Profile > Applications > Personal access tokens. The examples use Bash, curl, and jq. The examples target the test instance. Replace the token with your own, and change the host if you use another instance; URL_BASE must not end with /.

URL_BASE='https://b2inst-test.gwdg.de'
TOKEN='<personal-access-token>'

The record ID used in API paths is the response’s id, not parent.id or the instrument Handle. Save returned IDs instead of copying IDs from example responses.

Create a metadata-only draft#

Save this request as instrument.json. The example adapts the sensor metadata in the correspondence and uses a valid https:// landing-page URL.

cat > instrument.json <<'JSON'
{
  "files": {"enabled": false},
  "access": {"record": "public", "files": "public"},
  "metadata": {
    "Name": "Soil moisture sensor 942 - HydraProbe Professional SDI-15",
    "SchemaVersion": "1.0",
    "InstrumentType": [
      {
        "instrumentTypeName": "Soil moisture and temperature sensor",
        "instrumentTypeIdentifier": {
          "instrumentTypeIdentifierType": "URL",
          "instrumentTypeIdentifierValue": "https://sms-cv.helmholtz.cloud/sms/cv/api/v1/equipmenttypes/599/"
        }
      }
    ],
    "Date": [{"Date": "2025-05-10", "dateType": "Commissioned"}],
    "Description": "Example device uploaded via API.",
    "LandingPage": "https://sms-cv.helmholtz.cloud/device/129",
    "Manufacturer": [{"manufacturerName": "Stevens Water Monitoring Systems"}],
    "Owner": [{"ownerName": "University of Rostock"}]
  }
}
JSON

curl --fail-with-body -sS -X POST "$URL_BASE/api/records" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @instrument.json > draft.json

RID=$(jq -er '.id' draft.json)
jq '{id, status, errors, metadata, access, files}' draft.json

Success returns HTTP 201. Inspect errors even when saving succeeds: a saved draft can still have validation errors that prevent publication. Saving or updating a draft does not publish it.

files.enabled: false selects a metadata-only record. For this public example the response reports access.status: "metadata-only"; that status is calculated by the service and does not need to be supplied. access.record and access.files remain the writable access settings.

Instrument identifiers#

Omit metadata.Identifier when creating a record: B2INST assigns it from the configured Handle prefix and the record ID, and replaces user-supplied values on draft updates. Keep inventory numbers or other instrument identifiers in metadata.AlternateIdentifier.

The following is an illustrative response fragment for a public record with an ePIC PID:

{
  "id": "2qzkm-nzp30",
  "metadata": {
    "Identifier": {
      "identifierType": "Handle",
      "identifierValue": "21.T11975/2qzkm-nzp30"
    }
  },
  "pids": {
    "epic": {
      "identifier": "http://hdl.handle.net/21.T11975/2qzkm-nzp30",
      "provider": "epic",
      "client": "epic"
    }
  }
}

metadata.Identifier.identifierValue is the bare Handle; pids.epic.identifier, when present, is its resolver URL. The metadata identifier alone does not prove registration with the Handle service. An unpublished draft normally has no registered ePIC PID. Publication-time PID creation depends on the instance configuration and record access; a missing pids.epic on an expected public, Handle-enabled publication should be reported to the service team. Other PID entries, such as oai, can also appear.

Upload files (optional)#

Skip this section for a metadata-only record. To add files to the draft created above, first enable them while preserving its full metadata:

jq '.files.enabled = true' instrument.json > instrument-with-files.json
curl --fail-with-body -sS -X PUT "$URL_BASE/api/records/$RID/draft" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @instrument-with-files.json > draft.json

An enabled draft needs at least one uploaded and committed file before it can be published. Complete all registered uploads before publishing.

Register, upload, and commit#

First register the file entry (HTTP 201):

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$RID/draft/files" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-raw '[{"key":"article.pdf"}]'

Upload the binary content with PUT to /content (HTTP 200). Replace ./article.pdf with your local file path:

curl --fail-with-body -sS -X PUT "$URL_BASE/api/records/$RID/draft/files/article.pdf/content" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/octet-stream' \
  --upload-file ./article.pdf

Then commit that same file key (HTTP 200):

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$RID/draft/files/article.pdf/commit" \
  -H "Authorization: Bearer $TOKEN"

Calling /commit does not upload content. Use the same key in registration, upload, and commit. For keys containing spaces or other reserved characters, URL-encode the key in request paths.

To register several files at once, replace the single-file registration body with this array, then upload and commit each key:

[
  {"key":"a.pdf"},
  {"key":"b.csv"},
  {"key":"c.txt"}
]

Check the uploads before publication:

curl --fail-with-body -sS "$URL_BASE/api/records/$RID/draft/files" \
  -H "Authorization: Bearer $TOKEN" | jq '.entries[] | {key, status}'

Each completed entry has status: "completed".

Find the community ID#

Read a community by slug or list available communities through GET /api/communities. The community name is in metadata.title; use the returned UUID in record and review requests.

COMMUNITY_SLUG='eudat'
curl --fail-with-body -sS "$URL_BASE/api/communities/$COMMUNITY_SLUG" \
  -H "Authorization: Bearer $TOKEN" > community.json
COMMUNITY_ID=$(jq -er '.id' community.json)
jq '{id, slug, title: .metadata.title, access}' community.json

Choose one of the following publication workflows. The checked deployment requires a community for ordinary users to publish; submission, inclusion, and publication also depend on your permissions and the community’s policies.

Publish directly with a community#

Use this workflow when you may both include the draft directly in the community and publish it. Adding a top-level communities array to POST /api/records does not attach the community. parent.communities is response data, not an input field for assignment.

After creating the draft, call the separate record-community endpoint. There is no /draft segment in this URL:

jq -n --arg community "$COMMUNITY_ID" \
  '{communities: [{id: $community}]}' > community-add.json

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$RID/communities" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @community-add.json > community-add-result.json

jq '.' community-add-result.json

A successful direct association returns HTTP 200 with a result like:

{
  "processed": [
    {
      "community_id": "e9b9792e-79fb-4b07-b6b4-b9c2bd06d095",
      "directly_added": true
    }
  ]
}

Check processed and any errors entries. HTTP 200 can include per-community failures, and a result with request_id instead of directly_added: true represents a community inclusion request, not confirmation of a completed association. For a new draft that requires review, choose the review workflow below. If an inclusion request is already pending, cancel it using its permitted POST /api/requests/{request-id}/actions/cancel action before starting a separate submission review.

Read the draft to confirm that the UUID is present in parent.communities.ids:

curl --fail-with-body -sS "$URL_BASE/api/records/$RID/draft" \
  -H "Authorization: Bearer $TOKEN" > draft.json
jq '.parent.communities' draft.json

Once the association, metadata, and any uploads are ready, publish:

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$RID/draft/actions/publish" \
  -H "Authorization: Bearer $TOKEN" > published.json
jq '{id, status, is_published, pids, communities: .parent.communities.ids}' published.json

Success returns HTTP 202 and the published record. Direct association and publication have separate permission checks: successful association does not by itself grant permission to publish.

Submit to a community for review#

For an unpublished first-version draft, set up its review receiver (HTTP 200):

jq -n --arg community "$COMMUNITY_ID" \
  '{receiver: {community: $community}, type: "community-submission"}' > review.json

curl --fail-with-body -sS -X PUT "$URL_BASE/api/records/$RID/draft/review" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @review.json > review-result.json
REQUEST_ID=$(jq -er '.id' review-result.json)

Submit through the record’s submit-review action (HTTP 202):

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$RID/draft/actions/submit-review" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-raw '{}' > submission-result.json
jq '{id, status, is_open, is_closed, links}' submission-result.json

This action uses receiver.community already stored by PUT /draft/review; do not put the receiver in the submission action’s body. Using the record action also runs submission validation and the community inclusion logic.

If your role and the community policy allow direct inclusion, submission can be accepted and published immediately. Otherwise the request stays submitted until an authorized community reviewer accepts it. A 202 response alone does not mean publication. Check the returned request status and the record’s publication state.

The review endpoint is for initial publication. It cannot create a new review for an already published record or a new-version draft; those drafts retain their parent community membership and use the publication action after editing.

Create the draft and its review together#

The correspondence also shows an alternative: include parent.review in the initial POST /api/records. Prepare instrument.json and obtain COMMUNITY_ID as above, then use this in place of the initial draft creation and separate PUT /draft/review:

jq --arg community "$COMMUNITY_ID" \
  '.parent = {review: {
    type: "community-submission",
    receiver: {community: $community}
  }}' instrument.json > instrument-with-review.json

curl --fail-with-body -sS -X POST "$URL_BASE/api/records" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @instrument-with-review.json > draft-with-review.json
RID=$(jq -er '.id' draft-with-review.json)
jq '{id, status, errors, review: .parent.review}' draft-with-review.json

This creates a draft and an unsubmitted review in one request (HTTP 201). It does not submit or publish the record, and the receiver is not yet an accepted community association. Continue with the POST /api/records/$RID/draft/actions/submit-review example above. To switch this draft to direct publication instead, follow the next section.

Change an unsubmitted review to direct publication#

A draft with an unsubmitted or open review cannot be published directly. If the review has not been submitted, remove it first (HTTP 204):

curl --fail-with-body -sS -X DELETE "$URL_BASE/api/records/$RID/draft/review" \
  -H "Authorization: Bearer $TOKEN"

Then follow direct community association and publication. Deleting the review does not attach its receiver community. An open/submitted review cannot be deleted through this endpoint; use the request’s permitted cancellation action first. Community policies and publication permissions still apply.

Edit a published record without creating a new version#

Can I amend a published record’s metadata without minting a new Handle? Yes. Open an edit draft with POST /api/records/{id}/draft, save the complete metadata with PUT /api/records/{id}/draft, and republish with POST /api/records/{id}/draft/actions/publish. This keeps the existing record ID, version, and registered ePIC Handle. Use this workflow for metadata corrections; POST /versions creates a separate version instead.

For a metadata correction, create or retrieve an edit draft with the same record ID. Set PUBLISHED_ID to the published record you want to edit:

PUBLISHED_ID='<published-record-id>'
curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$PUBLISHED_ID/draft" \
  -H "Authorization: Bearer $TOKEN" > edit-draft.json
RID=$(jq -er '.id' edit-draft.json)

The response is HTTP 201. Receiving the same ID is expected. This action prepares the draft; its request body does not update metadata.

Construct a complete update from the returned draft, changing only the desired field. Preserve all metadata, including any community extension values. Omit response-only fields such as id, links, parent, status, and errors:

jq '{
  metadata,
  custom_fields,
  access: (.access | del(.status)),
  files: (.files | {enabled, default_preview, order})
} | .metadata.Description = "Updated device description after calibration."' \
  edit-draft.json > update.json

curl --fail-with-body -sS -X PUT "$URL_BASE/api/records/$RID/draft" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @update.json > updated-draft.json
jq '{id, status, errors}' updated-draft.json

PUT returns HTTP 200. It replaces the metadata object: sending only the changed metadata field can remove other fields. JSON Patch (PATCH with application/json-patch+json) is not supported for draft metadata. A PATCH endpoint elsewhere in the API, such as access-link management, does not change this rule.

After checking and correcting any validation errors, publish the edit:

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$RID/draft/actions/publish" \
  -H "Authorization: Bearer $TOKEN" > published.json
jq '{id, status, is_published, handle: .pids.epic.identifier}' published.json

The update payload above omits pids. On republication, the service preserves the published record’s existing required ePIC PID, including when that PID is absent from the edit draft’s response. To confirm it is unchanged, compare pids.epic.identifier on the published record before and after editing. If the record had no registered ePIC PID before editing, publication can create a missing PID according to the instance’s configuration. Existing file content is locked in this workflow; create a new version to change the file set.

Create a new version#

For a new version, use POST /versions, starting from the latest published record’s ID:

PUBLISHED_ID='<latest-published-record-id>'
curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$PUBLISHED_ID/versions" \
  -H "Authorization: Bearer $TOKEN" > version-draft.json
DRAFT_ID=$(jq -er '.id' version-draft.json)
jq '{id, versions, parent_id: .parent.id, metadata, files}' version-draft.json

The response is HTTP 201. The first call creates a draft with a new record ID and copied metadata. Repeating it while the next-version draft remains open returns that existing draft, rather than allocating another one. There can be only one open next-version draft for the parent. Supplying new metadata in this POST body has no effect; update the returned draft with a separate PUT.

The draft retains the shared parent and its community membership. A newly created version draft starts with an empty file set. A reused draft can already contain files; remove its file entries before switching it to metadata-only. For a metadata-only version, prepare and save a complete update as follows:

jq '{
  metadata,
  custom_fields,
  access: (.access | del(.status)),
  files: {enabled: false}
} | .metadata.Name = "Soil moisture sensor 127 - HydraProbe Professional SDI-12"
  | .metadata.LandingPage = "https://sms-cv.helmholtz.cloud/device/127"
  | .metadata.InstrumentType[0].instrumentTypeIdentifier.instrumentTypeIdentifierValue = "https://sms-cv.helmholtz.cloud/sms/cv/api/v1/equipmenttypes/57/"
  | .metadata.Date = [{Date: "2024-09-10", dateType: "Commissioned"}]' \
  version-draft.json > version-update.json

curl --fail-with-body -sS -X PUT "$URL_BASE/api/records/$DRAFT_ID/draft" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @version-update.json > updated-version.json
jq '{id, status, errors}' updated-version.json

If the new version should contain files, set files.enabled to true in version-update.json before the PUT. Then either register, upload, and commit files with RID="$DRAFT_ID", or import the previous version’s files into the empty draft (HTTP 201). Keep the metadata you just saved; the earlier instrument.json is only for the initial creation example.

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$DRAFT_ID/draft/actions/files-import" \
  -H "Authorization: Bearer $TOKEN"

Import must happen before adding other files to the draft. It returns the imported file list; the files do not need to be uploaded again. The previous version must have files available for import.

After resolving validation errors and completing any uploads, publish the new draft ID:

curl --fail-with-body -sS -X POST "$URL_BASE/api/records/$DRAFT_ID/draft/actions/publish" \
  -H "Authorization: Bearer $TOKEN" > published-version.json
jq '{id, status, is_published, versions, pids}' published-version.json

A successful PUT only saves the draft. The final POST is required to publish it.

Validation and common errors#

Symptom What to check
A saved draft returns errors Inspect the response after POST/PUT even on HTTP 201/200. Fix errors before publishing.
metadata.LandingPage: Not a valid URL. Use a complete URL such as https://...; the ttps://... typo in the correspondence is invalid.
Publish fails on missing instrument metadata Supply Name, a valid LandingPage, at least one named Manufacturer, and at least one named Owner. Model is recommended, not mandatory. Community extensions can add requirements.
files.enabled: missing uploaded files Upload and commit all files, or use files.enabled: false for a record with no files. Remove existing draft file entries before disabling files. Setting access.status alone does not disable files.
Direct publish reports that a review exists Submit that review, or remove an unsubmitted review and follow the direct workflow. Open reviews must be handled through permitted request actions.
A newly created record has no community Use the separate community association endpoint or the review workflow. Creation-time communities and parent.communities do not assign membership.
Community association succeeds but publishing is denied Inclusion and publication permissions are distinct. Check your community role; ordinary users also need an attached community in this deployment.
POST to /draft returns the original ID Expected for same-version metadata editing. Use /versions only when you need a new version.
POST to /versions ignores supplied metadata Use the returned draft ID and send the complete metadata object with PUT to /draft.

See the Records API, Communities API, and Requests API for endpoint details.

Last update : 22.09.2026

Last review : 22.09.2026