5
api client
holo edited this page 2025-06-18 23:17:28 +10:00

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).

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 PUT method is meant to be an idempotent version of the creation method POST, used to update an existing resource or create a resource with a given ID. We do not follow this. We currently only use PUT in one route, which is to upload a list of stamps (e.g. adding multiple stamps to a document at once).
  • The POST method is supposed to always create a new resource if possible. However, POST /users specifically does not create a new resource if a user with the given twitch_id already exists, and it instead updates the user, similar to a PATCH request.
  • 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 /users will return a User which has a user_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

...

API Reference