Authentication & API Tokens
The Public API (reading and downloading the dataset) requires no authentication — it stays open to everyone.
Writing data — submitting or updating experiment records from a script, notebook, or CI pipeline — requires a personal access token (PAT). This is the same mechanism the Python client uses.
[!TIP] For an exhaustive list of every REST endpoint and Python client method, see the Complete API Reference.
Creating a token
- Log in at electrospinning-data.org.
- Open Profile Settings → API Tokens.
- Give the token a name (e.g. "Lab laptop" or "CI pipeline") and choose an expiry (30, 90, 365 days, or no expiry).
- Click Create token.
[!IMPORTANT] The token's full value is shown only once, immediately after creation. Copy it somewhere safe (e.g. a password manager or CI secret store) — the server only ever stores a hash of it, so it cannot be recovered later. If you lose it, revoke it and create a new one.
A token looks like esd_pat_<random string>. Treat it like a password: anyone holding it can submit data as you.
Using a token
Send it as a standard bearer token in the Authorization header of any write request.
- cURL
- Python (requests)
- Python client
curl -X POST "https://api.electrospinning-data.org/data/submit" \
-H "Authorization: Bearer esd_pat_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": true},
"researchMetadata": {"publicationTitle": "My study", "doi": "10.1234/example"},
"experimentData": [{ "polymerProperty": { }, "processParameter": { } }]
}'
import requests
response = requests.post(
"https://api.electrospinning-data.org/data/submit",
headers={"Authorization": "Bearer esd_pat_your_token_here"},
json={
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": True},
"researchMetadata": {"publicationTitle": "My study", "doi": "10.1234/example"},
"experimentData": [{"polymerProperty": {}, "processParameter": {}}],
},
)
response.raise_for_status()
print(response.json())
from electrospinning_data_client import Client
client = Client(token="esd_pat_your_token_here")
result = client.submit({
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": True},
"researchMetadata": {"publicationTitle": "My study", "doi": "10.1234/example"},
"experimentData": [{"polymerProperty": {}, "processParameter": {}}],
})
print(result)
[!NOTE]
Clientis the current name of the Python client (ElectrospinningDataClientstill works as an alias, andsubmit_experiment/update_experiment/etc. still work as full-name equivalents ofsubmit/update/etc.) — see the Python Client Guide for the full mapping.
/data/submit, /data/update/{id}, and /data/update-submission/{id} all return a JSON body describing the outcome per record, e.g.:
{
"message": "Data submitted successfully",
"submissionId": 512,
"records": [
{ "recordId": 1044, "status": "PENDING", "missingFields": [] }
]
}
Submitted records go through the same moderation queue as web-form submissions, but are attributed to your account instead of being anonymous.
Record statuses & incomplete submissions
A submitted experiment record has one of four statuses. GET /data/statuses (no auth required) returns the current list programmatically.
| Status | Meaning |
|---|---|
PENDING | Complete and awaiting moderator review. |
APPROVED | Reviewed and published to the public dataset. |
REJECTED | Reviewed and declined by a moderator. |
NEEDS_UPDATE | Saved, but missing mandatory scientific fields (see below) — not yet in the moderation queue. |
Incomplete submissions (NEEDS_UPDATE). When you submit with a personal access token and the payload is missing a mandatory field — currently: polymer information, or the name of a solvent component you did include — the record is not rejected. It's saved with status NEEDS_UPDATE, and the response tells you exactly what's missing:
{
"message": "Data submitted successfully",
"submissionId": 513,
"records": [
{
"recordId": 1045,
"status": "NEEDS_UPDATE",
"missingFields": ["polymerProperty.polymerComponents[].polymerName"]
}
]
}
This leniency only applies to missing fields, and only for PAT-authenticated requests — anonymous/web-form submissions keep today's strict validation (a 400 error) since they have no way to come back and finish the record. A field that's provided but invalid (e.g. a polymer name that doesn't exist in our reference list) is always a hard 400 error, in every mode — that's a data-quality problem, not incompleteness.
To complete a NEEDS_UPDATE record, PUT /data/update/{recordId} with the missing fields filled in:
client.update(1045, {
"userMetadata": {"name": "Jane Doe", "email": "jane@example.com", "consentTerms": True},
"experimentData": [{
"polymerProperty": {"polymerComponents": [{"polymerName": "PVA"}]},
}],
})
# -> {"recordId": 1045, "status": "PENDING", "missingFields": [], "message": "..."}
Once all mandatory fields are present, the record automatically flips back to PENDING and re-enters the moderation queue.
Listing your own records
Query the records you've submitted — filtered to a single status (e.g. just the ones that NEEDS_UPDATE), or all together. Both endpoints require authentication (session or PAT) and only ever return records you own.
| Endpoint | Returns |
|---|---|
GET /data/my-records | Full record data (same shape as submit's experimentData items). |
GET /data/my-records/ids | Just the record ids — cheaper if you only need to know what to look at. |
Both accept an optional status query parameter (PENDING, APPROVED, REJECTED, or NEEDS_UPDATE). Omit it to get every record regardless of status.
- cURL — ids needing an update
- cURL — everything
- Python client
curl "https://api.electrospinning-data.org/data/my-records/ids?status=NEEDS_UPDATE" \
-H "Authorization: Bearer esd_pat_your_token_here"
# -> [1045, 1052]
curl "https://api.electrospinning-data.org/data/my-records" \
-H "Authorization: Bearer esd_pat_your_token_here"
# -> full record data for every status combined
# Just the ids of your incomplete records
needs_update_ids = client.record_ids(status="NEEDS_UPDATE")
# Full data for your incomplete records
needs_update_records = client.records(status="NEEDS_UPDATE")
# Everything, any status
all_records = client.records()
This complements /data/my-submissions (which groups your records by submission, with no status filter): /data/my-records gives you a flat, filterable list at the individual record level.
Revoking and regenerating
From Profile Settings → API Tokens you can:
- Revoke a token immediately — any script still using it will start getting
401 TOKEN_REVOKEDerrors. - Regenerate a token — keeps the same name and expiry, but issues a brand-new secret (the old one stops working immediately). Useful if a token may have leaked.
Error responses
Every write request with a bad token gets a 401 with a JSON body identifying exactly what went wrong:
| Situation | HTTP status | code |
|---|---|---|
No Authorization header at all | depends on the endpoint (public endpoints just proceed unauthenticated) | — |
| Token doesn't exist / malformed | 401 | TOKEN_INVALID |
| Token was revoked | 401 | TOKEN_REVOKED |
| Token expired | 401 | TOKEN_EXPIRED |
{
"success": false,
"message": "This API token has expired.",
"code": "TOKEN_EXPIRED"
}
Notes for the future
Tokens currently carry a single WRITE scope — everything a token can do today is submit or update experiment data on your behalf. The token model supports additional scopes being added later (e.g. read-only tokens, admin actions) without requiring you to recreate existing tokens.