Sending feedback from inside a game
The contract for a game that lets its players report a bug, send an idea, or complain. Both of this site's games use it: TGSC (Touching Grass Simulator Coop, on PC) and Touch Grass (on Android). They are separate games with similar names, and each has its own address and its own queue.
One call, one form, and the site sorts it. There is no account and no token - a game cannot ask a player to sign in to a website before telling you the game crashed.
Base address: https://dasmaffin.com.
Send one
POST /TouchGrass/api/report (Touch Grass, Android)
POST /Tgsc/api/report (TGSC, PC)
Content-Type: multipart/form-data
Does it need a key?
Every game chooses one of three, on its own settings page, and Open is the default - which is how both games have always worked and what they still do until somebody changes it.
| Mode | Send this | For |
|---|---|---|
| Open | nothing | Anybody can file a report. No key to ship, no key to leak |
| Public key | X-Api-Key: dmp_... |
A game with no server of its own. Ships inside the game |
| Private key | X-Api-Key: dms_... |
A game whose own server relays the report |
A public key is an identifier, not a secret: it is in every copy of the game and anybody can read it out. It is worth having so one build can be turned off without turning the site off, and it is worth nothing as protection. Do not build anything on it being private, and do not use it where a private key belongs.
Refusals carry a word to branch on: 401 with {"error":"no_key"} when none arrived, and
{"error":"bad_key"} when the one that arrived was not this game's. They are told apart because a
build agent that reads one bare 401 as "our key was rotated" will stop publishing over a shell
that ate a header.
The same key covers replies. It does not replace the separate issued key the private read API needs - see reading, which is gated on its own grant in every mode above.
The address decides the queue. Nothing in the body says which game this is; the site stamps it from the endpoint you posted to. A client cannot file into the other game's queue, by accident or otherwise.
Both addresses are the same endpoint under a different name: a module on this site declares that it
takes feedback, and the site answers POST /{module}/api/report for it.
A third game would appear here without a line of new intake code, and would behave exactly as these
two do.
The fields
| Field | Touch Grass | TGSC | Meaning |
|---|---|---|---|
playerId |
required | - | Who is sending. The Unity Gaming Services player id, the same one the save is filed under |
steamId |
- | required | Who is sending. A SteamID64, digits only |
summary |
required | required | One line. Up to 300 characters |
description |
required | required | What happened, in their words. Up to 8,000 |
kind |
optional | optional | Bug, Idea or Other. Anything else becomes Other |
playerName |
optional | optional | What they call themselves in game. A label, not identity |
build |
optional | optional | Your version string |
scene |
optional | optional | Where they were - the screen or level |
facts |
optional | optional | Device, OS, settings, anything else worth knowing. Up to 8,000 |
edition, role, world |
- | optional | PC game concepts; leave them out |
Anything longer than its limit is cut, not refused - a report is worth having with a truncated log.
summary and description are both required. An empty report costs nothing to send, arrives
looking like feedback, and can only be answered by guessing.
Attachments
One field per kind, and each takes as many files as you like. The field name says what a file
is; the file name is only a label for whoever reads the report afterwards. A part sent as logs
carrying whatever.json is a log. A part sent as notes carrying screenshot.png is not a
picture.
| Send it as | What it is | Where it appears on the report |
|---|---|---|
images |
Pictures | Shown, full size on click |
logs |
Log files | The first is shown as text on the page; every one is downloadable |
charts |
Measurements over time | Plotted - see charts. Also downloadable |
saves |
A save or world file | Under Files, as a download |
benchmarks |
A finished benchmark run | Shown as text |
files |
Anything else | Under Files, as a download |
Repeat a field to send several: two parts both named logs are two logs, and there is no numbering
scheme to follow. Multipart allows a repeated field name, so nothing has to be invented per file.
curl -X POST https://dasmaffin.com/TouchGrass/api/report \
-F "playerId=..." -F "summary=..." -F "description=..." \
-F "logs=@session.json;type=application/json" \
-F "logs=@previous-session.json;type=application/json" \
-F "images=@shot1.png;type=image/png" \
-F "images=@shot2.png;type=image/png" \
-F "charts=@metrics.json;type=application/json"
A field name that is not on that list lands under Files, as a plain download. Nothing is guessed from the file's name or its content: a file the site was not told the kind of is one it will not describe. It used to guess "picture", which put a game's second log in the Pictures section as a broken image.
The older names still work, because both of this site's games send them and neither should
break before it ships a new build: shot, log, metrics, save, benchmark. That is the whole
list - exactly what those two emit, not a set of spellings that might be accepted. Anything new
should use the fields in the table, which are the kinds themselves.
Set Content-Encoding: gzip on a part and it is unpacked before it is stored, so what somebody
opens is the plain thing. Compress for the upload's sake, not for the reader's.
Charts
A file part named metrics is plotted on the report's page. Its shape is its own document:
charts.
What comes back
{ "id": 412 }
| Code | Meaning |
|---|---|
200 |
Stored. The id is the report's |
400 |
No sender id, or a missing summary or description |
401 |
This game needs a key and none came (no_key) or the wrong one did (bad_key) |
403 |
That player is barred from sending. Body: {"banned":true,"reason":"..."} - show the reason |
413 |
Everything together came to more than 16 MB |
429 |
More than 12 reports from that player in the last hour |
A 403 is worth handling properly: the reason is written for the player and is the only explanation
they will get. Touch Grass has no ban list today, so it will not see one - the code is documented
because the endpoint can answer it the moment there is one, and a client that treats an unknown
status as "it worked" is a client that silently loses reports.
An example
curl -f -X POST \
-F "playerId=Ax7Bq2Kd9Lm4Np6Rs8Tv0Wx2Yz4A" \
-F "summary=Garden resets after closing the app" \
-F "description=Planted three plots, closed the app from recents, came back and the garden was empty." \
-F "kind=Bug" \
-F "playerName=Someone" \
-F "build=1.0.4" \
-F "scene=Garden" \
-F "facts=Pixel 7a, Android 15, English" \
-F "log=@player.log;type=text/plain" \
https://dasmaffin.com/TouchGrass/api/report
Reading them back
Two different jobs, and they are not the same document.
A game client showing a player their own reports, the replies written back, and letting them answer or close a thread: replies. No API key, because the caller is the player. TGSC only today, and that document says exactly why and what Touch Grass would need.
A program on the project's side reading the whole queue: reading. It needs a private API key, and so does its documentation.
What happens to it
It lands in that game's queue on this site, where somebody reads it, can answer it, and can close it. Reports are deleted after 120 days.
The player id is what the game claimed, not something this site proved, and it is treated that way: it is good for grouping one person's reports together and for answering them, and for nothing else. Send the id you already have rather than inventing one per report, or two reports from the same person cannot be recognised as such.
Nothing here needs an account, on either side. What a player types is what arrives.