Dreamspace API Usage Overview
Getting Started
The API Format
This API supports CRUD (Create, Read, Update, and Delete) operations for the objects and models of 'Dreamspace Adventures'. (See index for a brief list of manipulable modules, or the below API Reference for more comprehensive detail.)
The REST standard and our deviations
This API very loosely follows an HTTP API architectural standard known as 'REST'. See the following resources for an introduction to this standard, the HTTP protocol, as well as the definition of some key terms used in HTTP API development (and this documentation).
- https://blog.postman.com/what-are-http-methods/
- https://restfulapi.net/http-methods/
- https://en.wikipedia.org/wiki/Query_string
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview
- Note that libraries such as
aiohttpin python andHTTPRequestin godot do 90% of the communication layer work described here.
- Note that libraries such as
Now that you have read that, you need to know the design of this API actually deviates in the following significant ways from the REST standard:
- The
PUTmethod is meant to be an idempotent version of the creation methodPOST, used to update an existing resource or create a resource with a given ID. We do not follow this. We currently only usePUTin one route, which is to upload a list of stamps (e.g. adding multiple stamps to a document at once). - The
POSTmethod is supposed to always create a new resource if possible. However,POST /usersspecifically does not create a new resource if a user with the giventwitch_idalready exists, and it instead updates the user, similar to aPATCHrequest. - We do not return any 'positive' response code except
200 OK. In particular we do not return '201 Created', '202 Accepted', '204 No Content'. This is an oversight on my part and may be remedied in future. - After creating a resource (e.g. via
POST /users), we do not return the location of the resource in the 'Location' header. Instead, we return the newly created resource, which will always include a unique ID that may be used to fetch it again. E.g.POST /userswill return a User which has auser_id. - None of the standard caching semantics apply. Due to the relatively low expected volume, we instead fetch almost every object freshly when requested. The only exception is stamp types, which are not exposed as objects to the client anyway.
Everything is JSON
The API only accepts JSON requests.
That is, the body of the request must be a JSON-formatted string, and the request headers should include:
Content-Type: application/json
Each request will return a JSON-encoded body as well. Either a JSON object for single-object endpoints (such as /users/1), or a JSON list for multi-object endpoints (such as /users or /document/1/stamps).
Dates and times
Dates and times are represented by strings using the ISO 8601 standard, for example 2025-06-08T20:12:00+10:00. Note that these should nearly always include the timezone offset (here +10:00), both upon sending and receiving, and the client is responsible for converting responses and queries to the correct timezones. If a timestamp is sent without a datetime offset, then it will be converted into the timezone of the server, which is not usually what you want.
Other notes
- No ratelimiting is implemented (but please don't continuously flood the server).
- If a target resource does not exist, we respond with
404, and will sometimes attach a human-readable error about what was not found. - If the request was malformed in a way we explicitly handle, (e.g. attempting to update a field which doesn't exist, or not including a required field), we respond with
400 Bad Request, and usually attach a human-readable error message. - If the request was malformed in a way we do not handle (e.g. unexpected issues decoding the JSON, or, in some cases, referencing objects which do not exist), we respond with
500 Internal Server Error, with no more information. This is also raised if there is a bug or issue on the server side because we cannot distinguish these two cases. - Note that the error information is not JSON-encoded.
Authentication
To ensure bad-actors cannot read and modify the data accessible through the API, we use the simplest implementation of authentication possible, namely APIKey.
Each request sent to the API must have the X-API-KEY header set, with the value being exactly the configured API key.
X-API-KEY: SecureToken
On the server side, the key is stored as APITOKEN under the configuration section [API]. The default key, used only for testing and experimenting, is SecureToken.
If authentication fails, then HTTP code 403 Not Authorized is returned.
Our Model Structure
This API manipulates 6 related models, namely User, Transaction, Specimen, Event, Document, and Stamp.
Each model (e.g. User) may mnemonically be thought of as stored in a 'directory' (e.g. /users), with 'files' or 'subdirectories' given by the unique ID of that object (e.g. /users/1). (Note that the UID used here is an internal unique ID roughly correlated to when the object was created on the server, not at all related to e.g. the twitch ID.)
The corresponding directories are then /users, /transactions, /specimens, /events, /documents, and /stamps.
The model inter-relationships may be summarised as follows:
- Users have events, and each Event has a user.
- Users have specimens, and each Specimen has an owner.
- Users have transactions, and each Transaction has a user.
- Events may have a document. Documents do not have an event.
- Documents have stamps, and each Stamp has a document.
These relationships are explicitly represented in the API, both in object structure and in URIs.
For example /users/1/transactions represents all the transactions for user number 1, and this path supports the same operations as /transactions.
Similarly, /events/1/document represents the document for event number 1, which may be useful for, for example, adding stamps after the event is created (POST /events/1/document/stamps)
CRUD operations
The following sections describes how to do each of Create, Read, Update, and Delete with this API.
I suggested playing around with the examples in Postman so you get a feel for how they work.
Getting objects
To list the contents of one of the model directories, use a GET request.
For example, GET /users will return all the users known by the API.
To filter these users, various search parameters are supported, which must be added as query parameters to the URI. For example, GET /users?twitch_id=100 will return a list of users whose twitch_id is 100 (in practice, this filter will return either one or no users). Search parameters may also be combined, for example, GET /events?event_type=plain&created_after=2025-01-01&document_seal=1 will return all the events with type plain, created in 2025, and with seal number 1. See the chapters in the API Reference below for the accepted search parameters for each model.
If you already know the UID of a desired object (for example if you are keeping a cache of twitch ids to user ids), then you can request it directly by doing, for example, GET /users/1.
Creating objects
If you want to create a new object (explicitly, more on that in a moment), then you need to use a POST request to the model directory, and you need to attach a payload containing the data to create. For example, to create a new user, you would do POST /users with the following body:
{
"twitch_id": "105474571",
"name": "alllenski",
}
This will create a new user with the given data, and return the created object to you (which will in particular have the internal UID which you can cache for later requests if you want). See the API reference for each model for which fields are allowed or required on creation.
For convenience, objects may also be created implicitly. This is best illustrated by contrast. The explicit creation flow for a plain message event with one stamp would require the follow requests (assuming no objects already existed).
Create the user:
POST /users
{
"twitch_id": "105474571",
"name": "alllenski",
}
Returns:
{
"user_id": 4,
"twitch_id": "105474571",
"name": "alllenski",
"preferences": null,
"created_at": "2025-06-13T23:52:45.320284+10:00",
"specimen": null,
"inventory": [],
"wallet": null
}
Create the document:
POST /documents
{
"document_data": "base 64 encoded image data",
"seal": 1,
"metadata": "Some arbitrary metadata"
}
Returns:
{
"document_id": 21,
"document_data": "base 64 encoded image data",
"seal": 1,
"created_at": "2025-06-18T21:27:59.053049+10:00",
"metadata": "Some arbitrary metadata",
"stamps": []
}
Add the stamp:
POST /stamps
{
"document_id": 21,
"stamp_type": "Approve",
"pos_x": 0,
"pos_y": 0,
"rotation": 0.0
}
Returns:
{
"stamp_id": 31,
"document_id": 21,
"stamp_type": "Approve",
"pos_x": 0,
"pos_y": 0,
"rotation": 0.0
}
Then finally create the event, using the UIDs of the objects created above:
POST /events
{
"document_id": 21,
"user_id": 4,
"user_name": "alllenski",
"occurred_at": "2025-06-08T20:12:00",
"event_type": "plain",
"message": "This is a test plain event message."
}
Returns:
{
"event_id": 21,
"document_id": 21,
"document": {
"document_id": 21,
"document_data": "base 64 encoded image data",
"seal": 1,
"created_at": "2025-06-18T21:27:59.053049+10:00",
"metadata": "Some arbitrary metadata",
"stamps": [
{
"stamp_id": 31,
"document_id": 21,
"stamp_type": "Approve",
"pos_x": 0,
"pos_y": 0,
"rotation": 0.0
}
]
},
"user_id": 4,
"user": {
"user_id": 4,
"twitch_id": "105474571",
"name": "alllenski",
"preferences": null,
"created_at": "2025-06-13T23:52:45.320284+10:00"
},
"user_name": "alllenski",
"occurred_at": "2025-06-08T20:12:00+10:00",
"created_at": "2025-06-18T21:33:12.248765+10:00",
"event_type": "plain",
"message": "This is a test plain event message."
}
However, the implicit creation flow, allows us to do all the above steps with one request, implicitly creating the document, its stamps, and the user if required.
POST /events
{
"document": {
"document_data": "base 64 encoded image data",
"seal": 1,
"metadata": "Some arbitrary metadata"
"stamps": [
{
"stamp_type": "approve",
"pos_x": 0,
"pos_y": 0,
"rotation": 0.0
}
]
},
"user": {
"twitch_id": "105474571",
"name": "alllenski",
},
"user_name": "alllenski",
"occurred_at": "2025-06-08T20:12:00",
"event_type": "plain",
"message": "This is a test plain event message."
}
Updating objects
To update an object, send a PATCH request to the path of the object. For example, to change the message of the event created in the previous section, you would do:
PATCH /events/21
{
"message": "This is a new message"
}
The server will respond with the full object that was updated.
PATCH does not generally support implicit operations like POST does above, with the exception of /documents/, which allows you to update and implicitly create the stamps field.
Deleting objects
To delete an object, simply send a DELETE request to path of the object. For example:
DELETE /events/21
The server will respond with the state of the object just before deletion.
Some basic connection examples
The below examples assume the API server is at https://dreams.thewisewolf.dev and the API key is set to SecureToken.
Get all users
In cURL:
curl -H "Accept: application/json" -H "Content-type: application/json" -H "X-API-KEY: SecureToken" -X GET https://dreams.thewisewolf.dev/users
Attempted example in Godot: See https://docs.godotengine.org/en/stable/classes/class_httprequest.html#class-httprequest and https://docs.godotengine.org/en/stable/tutorials/networking/http_request_class.html#setting-custom-http-headers
Spawn a specimen for a user
...
Create a subscription event
...