gzip -c outcomes.json | curl --request POST \
--url "https://rtb.example.com/v1/outcomes" \
--header "Authorization: <api-key>" \
--header "Content-Type: application/json" \
--header "Content-Encoding: gzip" \
--data-binary @-
import gzip, json
import requests
body = json.load(open("outcomes.json"))
response = requests.post(
"https://rtb.example.com/v1/outcomes",
headers={
"Authorization": "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(body).encode("utf-8")),
)
result = response.json()
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
const response = await fetch("https://rtb.example.com/v1/outcomes", {
method: "POST",
headers: {
Authorization: "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
body: gzipSync(readFileSync("outcomes.json")),
});
const result = await response.json();
{
"outcomes": [
{
"placement_id": "example-4390001",
"gall_id": "example_gallery",
"post_no": 31899120,
"tier": "main",
"placed_at": "2026-09-29T02:40:00+09:00",
"picked_by": {
"type": "model",
"model_version": "m0924-1",
"weights_version": "w-1",
"cycle_id": "20260929T0240"
},
"score_at_placement": 0.62,
"views_at_placement": 1875,
"up_at_placement": 40,
"down_at_placement": 3,
"comment_at_placement": 21,
"readings": [
{
"age_h": 1,
"observed_at": "2026-09-29T03:40:00+09:00",
"views": 9120,
"up": 66,
"down": 8,
"comment_cnt": 30
}
],
"hidden_at": null,
"deleted_at": null,
"deleted_by": null
}
]
}
{
"received": 1,
"created": 1,
"updated": 0
}
{
"error": {
"code": "bad_request",
"message": "2 fields failed validation",
"fields": [
{
"path": "candidates[0].created_at",
"problem": "missing +09:00 offset"
},
{
"path": "engagement[3].views",
"problem": "must be a non-negative integer or null"
}
]
}
}
Learning API
Send Outcomes
How placed posts performed, one record per placement, with readings at 1, 6, 24 and 48 hours. This is what the model learns from.
POST
/
v1
/
outcomes
gzip -c outcomes.json | curl --request POST \
--url "https://rtb.example.com/v1/outcomes" \
--header "Authorization: <api-key>" \
--header "Content-Type: application/json" \
--header "Content-Encoding: gzip" \
--data-binary @-
import gzip, json
import requests
body = json.load(open("outcomes.json"))
response = requests.post(
"https://rtb.example.com/v1/outcomes",
headers={
"Authorization": "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(body).encode("utf-8")),
)
result = response.json()
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
const response = await fetch("https://rtb.example.com/v1/outcomes", {
method: "POST",
headers: {
Authorization: "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
body: gzipSync(readFileSync("outcomes.json")),
});
const result = await response.json();
{
"outcomes": [
{
"placement_id": "example-4390001",
"gall_id": "example_gallery",
"post_no": 31899120,
"tier": "main",
"placed_at": "2026-09-29T02:40:00+09:00",
"picked_by": {
"type": "model",
"model_version": "m0924-1",
"weights_version": "w-1",
"cycle_id": "20260929T0240"
},
"score_at_placement": 0.62,
"views_at_placement": 1875,
"up_at_placement": 40,
"down_at_placement": 3,
"comment_at_placement": 21,
"readings": [
{
"age_h": 1,
"observed_at": "2026-09-29T03:40:00+09:00",
"views": 9120,
"up": 66,
"down": 8,
"comment_cnt": 30
}
],
"hidden_at": null,
"deleted_at": null,
"deleted_by": null
}
]
}
{
"received": 1,
"created": 1,
"updated": 0
}
{
"error": {
"code": "bad_request",
"message": "2 fields failed validation",
"fields": [
{
"path": "candidates[0].created_at",
"problem": "missing +09:00 offset"
},
{
"path": "engagement[3].views",
"problem": "must be a non-negative integer or null"
}
]
}
}
Overview
Not in MVP scope. Outcomes are documented here for later. The first trial does not depend on this call, and nothing in the cycle call needs it.
The 48-hour reading is what makes a post a training label. Send the earlier readings as they become available, and send the same
placement_id again to add later ones. That updates the record.Net counts are the reading minus the count at placement. Readings on the board copy start from the origin post’s values, so
views_at_placement must be exact, from the board’s origin_hit.Cadence & freshness
Refresh
Daily during the dress rehearsal, for operator-placed posts. In the live trial, as they occur or in hourly batches, to be agreed.
All or nothing
If any record fails validation, nothing in the call is stored, and the error lists every failing path.
Hides and deletions. DCinside’s admin does not record when a placed post is hidden, only the
hidden_after_exposure label, so send that label and leave hidden_at as null until timed hides are logged through the unpublish path. Deletions come from the deletion table: deleted_by, deleted_at, and deleted_relative, which says whether the deletion came before or after the operator’s decision.picked_by and score_at_placement let us compare them on the same board. The model’s history features use only Main and Light outcomes that are at least 48 hours old, and they take the post’s original title from the stored candidate record.Request Attributes
The body is gzip-compressed JSON.array of objects
required
Up to 1,000 records. Each record is one placement.
Show item properties
Show item properties
string
required
DCinside’s id for this placement. Sending it again with more readings updates the record. Delivered files:
selected_id.string
required
Post key, together with
post_no.integer
required
Post number. Unique only within a gallery, so a post is always keyed by
gall_id and post_no together.string
required
The board the post was placed on. Delivered files:
rtb_head_tag. One of main, light, night, app.string
required
When the post went onto the board. Also fixes the day and time block used for outcome percentiles. Delivered files:
exposed_at.object
required
Who chose the post:
{"type": "operator"}, or {"type": "model", "model_version", "weights_version", "cycle_id"}.Show chosen by an operator
Show chosen by an operator
string
required
The post was chosen by an operator.
number | null
The publish score the post had when placed. Lets us compare model and operator picks on the same board.
integer
required
Exact, from the board’s
origin_hit. Net views are readings minus this. Delivered files: rtb_start_counts.views_at_placement.integer | null
Last gallery-side up-votes before placement.
integer | null
Last gallery-side down-votes before placement.
integer | null
Last gallery-side comment count before placement.
array of objects
required
Up to four readings, at 1, 6, 24 and 48 hours. Send them as they become available.
Show item properties
Show item properties
integer
required
Hours since placement: 1, 6, 24 or 48. One of
1, 6, 24, 48.string
required
When the counters were read.
integer | null
required
Cumulative views on the board copy.
integer | null
required
Cumulative up-votes on the board copy.
integer | null
required
Cumulative down-votes on the board copy.
integer | null
required
Cumulative comments on the board copy.
boolean | null
The label DCinside has today: the post was hidden after it was exposed. Send it now, and use
hidden_at once timed hides exist.string | null
When the post was taken down. DCinside’s admin does not record this today, so send
null until timed hides are logged through the unpublish path.string | null
When the post was deleted, if it was. Delivered files:
deletion.deleted_at.string | null
Who deleted it:
author, admin or auto. Delivered files: deletion.deleted_by. One of author, admin, auto.string | null
Whether the deletion came before or after the operator’s decision. Only
after_decision is an outcome of the placement. Delivered files: deletion.deleted_relative. One of before_decision, after_decision.Response Attributes
integer
required
Records in the request.
integer
required
Records stored as new placements.
integer
required
Records that matched an existing
placement_id and updated it.gzip -c outcomes.json | curl --request POST \
--url "https://rtb.example.com/v1/outcomes" \
--header "Authorization: <api-key>" \
--header "Content-Type: application/json" \
--header "Content-Encoding: gzip" \
--data-binary @-
import gzip, json
import requests
body = json.load(open("outcomes.json"))
response = requests.post(
"https://rtb.example.com/v1/outcomes",
headers={
"Authorization": "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(body).encode("utf-8")),
)
result = response.json()
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
const response = await fetch("https://rtb.example.com/v1/outcomes", {
method: "POST",
headers: {
Authorization: "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
body: gzipSync(readFileSync("outcomes.json")),
});
const result = await response.json();
{
"outcomes": [
{
"placement_id": "example-4390001",
"gall_id": "example_gallery",
"post_no": 31899120,
"tier": "main",
"placed_at": "2026-09-29T02:40:00+09:00",
"picked_by": {
"type": "model",
"model_version": "m0924-1",
"weights_version": "w-1",
"cycle_id": "20260929T0240"
},
"score_at_placement": 0.62,
"views_at_placement": 1875,
"up_at_placement": 40,
"down_at_placement": 3,
"comment_at_placement": 21,
"readings": [
{
"age_h": 1,
"observed_at": "2026-09-29T03:40:00+09:00",
"views": 9120,
"up": 66,
"down": 8,
"comment_cnt": 30
}
],
"hidden_at": null,
"deleted_at": null,
"deleted_by": null
}
]
}
{
"received": 1,
"created": 1,
"updated": 0
}
{
"error": {
"code": "bad_request",
"message": "2 fields failed validation",
"fields": [
{
"path": "candidates[0].created_at",
"problem": "missing +09:00 offset"
},
{
"path": "engagement[3].views",
"problem": "must be a non-negative integer or null"
}
]
}
}
Authorizations
The API key issued for the environment, sent as the whole header value. Two keys are valid during a rotation.
Headers
Send gzip when the body is gzip-compressed. Recommended for this call: a busy cycle is about 0.6 MB of JSON.
Available options:
gzip Body
application/json
Up to 1,000 records. Each record is one placement.
Maximum array length:
1000Show child attributes
Show child attributes