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, the record is not rejected. The mandatory fields are the ones the web form requires: at least one polymer and one solvent (each with a name), the solution concentration, the needle type with its gauge(s), the collector type (plus the rotation speed for drums and mandrels, the gap for parallel electrodes, and so on), voltage, flow rate, tip-collector distance and fiber diameter, a weight ratio for every polymer in a blend and a volume ratio for every solvent in a mixture, and a unit for every value you give. 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 is always a hard 400 error, in every mode — that's a data-quality problem, not incompleteness. Examples: a polymer name that doesn't exist in our reference list, an unknown unit, a zero or negative voltage, humidity above 100 %, or a diameter range whose lower value is larger than its upper one. The error message names the field.
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.