Depositing to Dataverse from Ekayana: What the Bridge Actually Does
The integration runs from Ekayana into Dataverse, not the other way round. What each endpoint does, which two credentials it takes, and the operations that do not exist.
The bridge runs the other way
Start with the direction, because everything downstream depends on it - and because it is the opposite of what most people assume.
No endpoint pulls a Dataverse dataset into Ekayana. Nothing takes a DOI, fetches that dataset's files and pins them. POST /api/dataverse/dataset runs the reverse: you hand it a did you already own, a dataverseAlias, and a metadata block, and it creates a new draft dataset in Dataverse, returning a freshly minted persistentId. Data starts on Ekayana and acquires a DOI over there.
If your data is already in Dataverse under a DOI that papers cite, the bridge still has something for you, but it is smaller than an import. POST /api/did/{did}/dataverse records that a DID you control and a DOI that already exists describe the same work. It moves no bytes. It is an association, and it is useful precisely because it is cheap - but calling it "importing your dataset" would misdescribe what happens on disk.
What you actually need
No SDK required. The platform is a plain HTTP/JSON API. Any language with an HTTP client works, so the honest prerequisite is curl, or fetch, or requests, or whatever you already have open. Node 18 is not required, because there is nothing to install.
Not an API key either. The platform issues none - the docs state that twice, in bold, on two separate pages. What you get is a session token from POST /api/signin:
curl -X POST https://api.ekayana.com/api/signin \
-H "Content-Type: application/json" \
-d '{ "email": "ada@lab.example", "password": "..." }'
# -> { "token": "..." }The token defaults to a 24-hour lifetime and rides on every subsequent request as Authorization: Bearer <token>. There is no refresh window; when it lapses you sign in again.
The one credential you have to go and fetch yourself is a Dataverse API token, issued by your own account on whichever Dataverse installation you are depositing to. That is the part of this integration most worth understanding, so it gets its own section rather than a bullet.
Two credentials, and we hold only one of them
The file deposit call carries both:
curl -X POST \
"https://api.ekayana.com/api/dataverse/dataset/file/doi%3A10.70122%2FFK2%2FABCDEF" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Dataverse-Token: $DATAVERSE_API_TOKEN" \
-H "X-Dataverse-Server: demo" \
-F "file=@reads.fastq.gz" \
-F "description=Raw sequencing reads"Authorization is your Ekayana session token. X-Dataverse-Token is your own Dataverse API token, which the platform never stores. X-Dataverse-Server selects the installation and defaults to demo - worth setting deliberately, unless you enjoy wondering where your dataset went.
That the second token is never stored is the good part of this design. The documented behaviour is narrow and worth quoting exactly: X-Dataverse-Token is your own Dataverse API token, which the platform never stores. So a deposit is something you authorise in the moment, with a secret you supply in the moment, rather than something a saved integration does on your behalf later.
Do not over-read that into a general guarantee. It says the platform does not persist this credential; it does not describe what happens to it in memory, in transit to Dataverse, or in logs, and the docs do not either. If your threat model needs those answers, ask for them rather than inferring them from this one sentence.
It also simplifies recovery. There is no configuration holding a Dataverse credential on our side, so if you regenerate that token you simply send the new value in the header on your next request. That is the whole remediation.
Mint the DID first
POST /api/dataverse/dataset consumes a DID rather than creating one, so the DID comes first. There is no createDID: true option anywhere; the identifier is derived from an Ed25519 public key you hold.
curl -X POST https://api.ekayana.com/api/did \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"controller": "did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc",
"public_key": "z6MkfuN2vWAoHermh6vY6TgAJfwhBWZCApZb9XeQ9HwLqHfz",
"service_endpoints": [],
"metadata": {
"title": "Coral bleaching survey 2026",
"researchers": [{ "name": "Ada Lovelace", "role": "PI" }],
"keywords": ["coral", "bleaching"],
"license": "CC-BY-4.0"
}
}'The server decodes the key, derives the identifier from it, and rejects anything that is not a valid Ed25519 key - so a DID can never disagree with its own subject key. GET /api/did lists what you hold; GET /api/did/resolve/{did} reads a document back without authentication.
Create the Dataverse dataset from it
curl -X POST https://api.ekayana.com/api/dataverse/dataset \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"did": "did:bio:123456789abcdefghi",
"dataverseAlias": "research-lab",
"metadata": {
"title": "CRISPR-Cas9 Gene Editing Dataset",
"authors": [{ "name": "Jane Smith", "orcid": "0000-0001-2345-6789" }],
"description": "Comprehensive dataset from CRISPR experiments...",
"keywords": ["CRISPR", "gene editing", "genomics"],
"license": "CC-BY-4.0"
}
}'What comes back is a datasetId, a newly assigned persistentId such as doi:10.7910/DVN/EXAMPLE, the did you passed in, a status of DRAFT, and links to both the Dataverse record and the IPFS object. The documented example for this call shows only the Ekayana bearer token; the Dataverse headers are spelled out on the file deposit call above.
DRAFT is the word to notice. Creating a dataset does not publish it.
Then the files, and pick a side on purpose
There are two deposits, they go to two different places, and no single call does both.
Uploading to Ekayana is POST /api/upload, multipart/form-data, one documented field:
curl -X POST https://api.ekayana.com/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@reads.fastq.gz"
# -> { "cid": "QmXyz...", "name": "reads.fastq.gz", "size": 20480, ... }That is the whole request: a file, and optionally a description. There is no did field and no metadata object on this endpoint. For anything large, add ?async=true and poll GET /api/upload/status/{task_id} until status is completed (with cid populated) or failed (with error). Content is pinned as part of the write, so there is no separate pin step, and no pinToIPFS flag to set: pinning happens because you supplied bytes, and it is the only way bytes get into IPFS here. GET /api/pins shows what you are holding.
Putting a file into the Dataverse dataset is the other call, POST /api/dataverse/dataset/file/{persistent_id}, shown above. Note that it is keyed by the URL-encoded DOI in the path, not by a DID, and it takes file plus an optional description.
When the draft is ready to become a citable version:
curl -X POST https://api.ekayana.com/api/dataverse/dataset/publish \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"datasetId": "doi:10.7910/DVN/EXAMPLE",
"did": "did:bio:123456789abcdefghi",
"type": "major",
"updateIPNS": true
}'Sync is one boolean
There is no sync-scheduling endpoint - no interval, no direction, nothing that reconciles the two systems on a timer.
Here is the entire sync surface reachable over HTTP:
curl -X POST "https://api.ekayana.com/api/did/did:bio:123456789/dataverse" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dataverseDoi": "doi:10.7910/DVN/EXAMPLE",
"syncMetadata": true,
"createIPNS": true
}'syncMetadata is a boolean. No direction, no per-dataset interval, no conflict-resolution parameter, no bidirectional toggle. The interval: '1h' and conflictResolution: 'latest-wins' you may have seen belong to dataverseConfig on the documentation page - a server-side deployment object populated from DATAVERSE_URL, DATAVERSE_API_TOKEN and DATAVERSE_ALIAS. That is how an operator configures an installation. It is not something a reader calls, and reading it as a client API is how the old version of this post ended up inventing an endpoint.
There is no manual sync trigger either, and nothing in the docs suggests an upload to Ekayana propagates to Dataverse by itself. The nearest real operation is POST /api/dataverse/dataset/publish, and publishing a version is a different action from reconciling metadata.
The bridge is described as offering bidirectional sync. The API underneath that description is one flag on one link call, and it is better to know that now than to build a release process around a scheduler that isn't there.
Enrichment is a separate call, by design
Metadata extraction is not an option on the deposit. The docs put it plainly: metadata extraction and knowledge graph enrichment run through the BioAgents endpoints as a separate step, not as flags on the deposit call. So extractMetadata: true was never a thing, and neither was the dataset.metadata.entities field the old post read back from it.
The real shape is a job you start and then poll. Deposit the file, take the CID, then:
curl -X POST https://api.ekayana.com/api/bioagents/process \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"file_cid": "QmXg9Pp2ytZ14xgK35M6iTC2Vz6jR9zYgooNp2UHPTMnPN",
"title": "CRISPR-Cas9 Gene Editing for Neurodegenerative Diseases",
"authors": ["Jane Smith", "John Doe"],
"doi": "10.1038/s41586-021-03819-2"
}'
# -> { "task_id": "b8e5c9a4-...", "status": "processing" }Then GET /api/bioagents/status/{task_id}. Extracted metadata comes back through POST /api/bioagents/metadata, and POST /api/bioagents/knowledge-graph builds the graph.
Splitting this out is the right call rather than an omission. Extraction is a pipeline with a variable runtime and a failure mode of its own; folding it into the deposit would mean your file upload fails because an NLP step timed out. Two calls, two outcomes, two things you can retry independently.
There is no custom field mapping
There is no custom metadata mapping either - no endpoint, no request field, no configuration key. Field names are whatever you put in the metadata block on dataset creation.
The one mapping table in the docs maps roles, not fields: owner to Admin, contributor to Contributor, curator to Curator, viewer to File Downloader. It is documentation of how permissions correspond across the two systems, and it is descriptive rather than callable. Do not mistake it for configuration.
In practice this matters less than it sounds, because you construct the metadata block yourself in the body of POST /api/dataverse/dataset. There is no automatic translation to override, so there is nothing to remap. If a field is not landing where you expect on the Dataverse side, the usual cause is that the installation does not have the relevant metadata block enabled, which is a Dataverse-side setting rather than anything you can change through this API.
If the dataset will change
Set createIPNS: true on the link call, or create the pointer directly with POST /api/ipns/create. Publish revisions by uploading the new bytes and repointing the name with POST /api/ipns/update; GET /api/ipns/{ipns_name}/versions returns every CID the name has ever pointed at. A paper citing version 1 keeps resolving to the exact bytes that were reviewed, because that CID is still pinned, while a new reader following the name gets the current revision.
When it fails
The documented error codes separate the two systems for you. A 401 with AUTH_ERROR refers to your Ekayana credential - a missing, expired or unverifiable session token, most often one that has aged past its 24 hours. A 403 means that credential is valid but does not authorise the action, so retrying will not help. A 502 with DATAVERSE_ERROR or EXTERNAL_SERVICE_ERROR means the Dataverse side failed, which is also where a rejected X-Dataverse-Token surfaces - it is Dataverse, not us, that adjudicates that token.
What the bridge is worth
A DOI that papers already cite keeps working, the DID gives the same dataset an identifier derived from a key you control, and the IPFS copy stays pinned so availability does not rest on one institution's uptime. Those three things are real and they compose.
What the bridge is not is a migration tool. It will not mirror an existing Dataverse holding into IPFS, it will not keep two systems in step without you, and it will not extract metadata as a side effect of a file upload. The narrower set of operations that does exist has the advantage of actually running.
For more details, see our Dataverse Integration documentation.
Ready to Get Started?
Explore our documentation to learn how to integrate Ekayana into your research workflow.