Depositing to Dataverse from Ekayana: What the Bridge Actually Does
The integration runs from Ekayana into Dataverse rather than 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. What the API does is create and fill datasets in Dataverse on your behalf. POST /api/dataverse/dataset takes a title, a description and a few optional fields, creates a draft dataset in a Dataverse collection, and returns the dataset's new persistent identifier. Files then go up one call at a time, from your machine through the API to Dataverse. Nothing moves from Ekayana's storage into Dataverse, and no call ties the new dataset to a DID for you.
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 the DOI on a DID you own and republishes the DID's public metadata document with it. It moves no bytes and never contacts Dataverse, so it checks that the DOI is well formed rather than that it exists. 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.
Not an API key either. The platform issues none. What you get is a session token, from POST /api/signin for an email and password account or from POST /api/wallet/signin for a wallet.
curl -X POST https://api.ekayana.io/api/signin \
-H "Content-Type: application/json" \
-d '{ "email": "ada@lab.example", "password": "..." }'
# -> { "token": "...", "user": { "id": 42, "username": "ada", "did": null, "wallet": null, ... } }The token lasts 24 hours by default and rides on every subsequent request as Authorization: Bearer <token>. There is no refresh. When it lapses you sign in again. Anything that creates an identity, a DID or a registered paper, also needs a wallet linked to the account, because identifiers are derived from it. Until one is linked, those calls answer 409.
The one credential you have to go and fetch yourself is a Dataverse API token, issued by your own account on the 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
Every Dataverse call that writes carries both.
curl -X POST \
"https://api.ekayana.io/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"
# -> { "file_id": "...", "message": "file uploaded" }Authorization is your Ekayana session token. X-Dataverse-Token is your own Dataverse API token. X-Dataverse-Server selects the installation. It takes demo, for demo.dataverse.org and the default, or harvard, for dataverse.harvard.edu, and refuses anything else. Set it deliberately, unless you enjoy wondering where your dataset went.
The docs describe the token in one sentence. It is used for the one request and not stored. 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.
The code goes a little further than that sentence. It forwards the token to Dataverse over HTTPS for that one request, and the request logs treat the header as sensitive and redact it. If your threat model needs more than that, ask rather than infer.
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.
The DID is a separate step
POST /api/dataverse/dataset never touches a DID, so the DID and the dataset are made separately, in either order. A DID comes from POST /api/did.
curl -X POST https://api.ekayana.io/api/did \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "dataset",
"metadata": {
"title": "Coral bleaching survey 2026",
"researchers": [{ "name": "Ada Lovelace", "role": "PI" }],
"keywords": ["coral", "bleaching"],
"license": "CC-BY-4.0"
}
}'The server derives the identifier from your wallet and a fresh random nonce, so there is no key to decode and none to lose. Nothing is on-chain until your wallet signs an anchor workflow, and until then GET /api/did/resolve/{did} answers notFound. GET /api/did lists what you hold, and resolution needs no authentication.
Create the Dataverse dataset
curl -X POST https://api.ekayana.io/api/dataverse/dataset \
-H "Authorization: Bearer $TOKEN" \
-H "X-Dataverse-Token: $DATAVERSE_API_TOKEN" \
-H "X-Dataverse-Server: demo" \
-H "Content-Type: application/json" \
-d '{
"title": "CRISPR-Cas9 Gene Editing Dataset",
"description": "Comprehensive dataset from CRISPR experiments",
"authors": ["Jane Smith"],
"keywords": ["CRISPR", "gene editing", "genomics"],
"dataverse_alias": "research-lab"
}'
# -> { "id": "12345", "persistent_id": "doi:10.70122/FK2/ABCDEF" }title and description are required. dataverse_alias picks the collection, and without it the dataset lands in the first collection your token can see. authors, keywords and subjects are lists of strings, and an empty subjects becomes "Medicine, Health and Life Sciences". The dataset contact is your account's username and email, because Dataverse requires one.
The dataset is created as a draft. Creating it 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 file part.
curl -X POST https://api.ekayana.io/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@reads.fastq.gz"
# -> { "cid": "bafybei...", "name": "reads.fastq.gz", "size": 48211306, ... }That is the whole request. There is no did field, no metadata object and no description 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. The file still arrives in that one request, so async saves you waiting for the sealing and pinning rather than lifting the upload limit. Content is pinned as part of the write, so there is no separate pin step and no pinToIPFS flag to set. GET /api/pins shows what you are holding.
The stored copy is sealed. Its CID names the encrypted envelope rather than your file, so it pins down which upload you mean, and it is not a checksum that anyone else could recompute from the data.
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 rather than 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.io/api/dataverse/dataset/publish \
-H "Authorization: Bearer $TOKEN" \
-H "X-Dataverse-Token: $DATAVERSE_API_TOKEN" \
-H "X-Dataverse-Server: demo" \
-H "Content-Type: application/json" \
-d '{ "persistent_id": "doi:10.70122/FK2/ABCDEF" }'
# -> { "persistent_id": "doi:10.70122/FK2/ABCDEF", "message": "dataset published" }Publishing always makes a major version.
There is no sync
There is no sync scheduling endpoint. There is no interval, no direction, and nothing that reconciles the two systems on a timer.
The link between a DID and a DOI is one call with one field.
curl -X POST "https://api.ekayana.io/api/did/did:bio:devnet:7Qm3.../dataverse" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "dataverse_doi": "doi:10.70122/FK2/ABCDEF" }'
# -> { "message": "DID linked to Dataverse dataset", "did": "did:bio:devnet:7Qm3...", "dataverse_doi": "10.70122/FK2/ABCDEF" }The only thing called sync is a workflow your wallet signs. The sync workflow copies the link into the DID document as a #dataverse service pointing at https://doi.org/<doi>, and until it runs the DID record shows pending_sync: true. Nothing ever writes back to Dataverse. PUT /api/dataverse/dataset/metadata updates a draft's metadata and POST /api/dataverse/dataset/publish publishes it, and both are calls you make.
If you have read that the bridge offers bidirectional sync, the API underneath has no such thing. 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, so extractMetadata: true was never a thing.
The real shape is a paper you register and then poll. Upload the file, take the CID, then:
curl -X POST https://api.ekayana.io/api/research-paper \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"file_cid": "bafybei...",
"title": "CRISPR-Cas9 Gene Editing for Neurodegenerative Diseases",
"authors": ["Jane Smith", "John Doe"],
"doi": "10.1038/s41586-021-03819-2"
}'
# -> { "message": "paper registered", "did": "did:bio:devnet:9Ls2...", "status": "pending" }Registration gives the paper a DID of its own and ingests it in the background. The text goes into the BioAgents knowledge base, and when you give a DOI the record's empty fields are filled from the knowledge graph. Poll GET /api/research-paper/did/{did} until status is completed, failed or skipped, and use POST /api/research-paper/{did}/ingest to run it again. The file must be one you uploaded yourself, and the knowledge base is searchable by every user of the agent, so register only text that may be read by everyone.
Splitting this out is the right call rather than an omission. Ingestion 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 indexing 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, and no free form metadata block either. The dataset call takes fixed fields, title, description, authors, keywords and subjects, and the API writes them into Dataverse's citation block, with your username and email as the contact. Any other metadata block your installation offers has to be filled in Dataverse itself.
If the dataset will change
Put an IPNS name in front of the upload. POST /api/ipns/create takes the cid of a file you uploaded and a file_name. POST /api/ipns/update?ipns_name=<name> takes the new file as multipart, seals and pins it, and moves the name in one call. GET /api/ipns/{name}/versions returns every CID the name has pointed at, newest first.
Updating never unpins an old version, so a citation of version 1 keeps pointing at the bytes that were reviewed for as long as you keep them. Those bytes are a sealed envelope, so a reader gets them through the API, as the owner or with a pass, rather than from IPFS directly.
When it fails
Errors come back as { "error": { "code": ..., "message": ... } }. A 401 with AUTH_ERROR refers to your Ekayana credential, meaning a missing, expired or unverifiable session token, most often one that has aged past its 24 hours. A 400 reading "missing x-dataverse-token header" means you left the Dataverse token off. A 403 with FORBIDDEN and "Dataverse rejected the API token" means Dataverse refused the one you sent, so a fresh token fixes it. Dataverse's own 400 and 404 come back as VALIDATION_ERROR and NOT_FOUND. A 502 with EXTERNAL_SERVICE_ERROR means Dataverse failed in some other way or timed out. A request body of the wrong shape is refused with a 422 before any of this, as plain text rather than the JSON envelope.
What the bridge is worth
A DOI that papers already cite keeps working, the DID gives the same work an identifier your wallet controls, and a sealed copy you upload to Ekayana does not depend on one Dataverse installation'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.