gzip -c cycle.json | curl --request POST \
--url "https://rtb.example.com/v1/cycles" \
--header "Authorization: <api-key>" \
--header "Content-Type: application/json" \
--header "Content-Encoding: gzip" \
--data-binary @-
import gzip, json
import requests
body = json.load(open("cycle.json"))
response = requests.post(
"https://rtb.example.com/v1/cycles",
headers={
"Authorization": "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(body).encode("utf-8")),
timeout=60,
)
scores = response.json()
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
const response = await fetch("https://rtb.example.com/v1/cycles", {
method: "POST",
headers: {
Authorization: "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
body: gzipSync(readFileSync("cycle.json")),
signal: AbortSignal.timeout(60_000),
});
const scores = await response.json();
{
"cycle_id": "20260929T0205",
"cycle_time": "2026-09-29T02:05:00+09:00",
"candidates": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"title": "Example original title",
"created_at": "2026-09-29T01:58:12+09:00",
"extracted_at": "2026-09-29T02:03:40+09:00",
"source": "keyword",
"manual_flag": 0,
"images_in_post": 2
}
],
"engagement": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"observed_at": "2026-09-29T02:04:00+09:00",
"status": "ok",
"views": 412,
"up": 9,
"down": 1,
"comment_cnt": 6,
"rtb_vote": 0
},
{
"gall_id": "example_gallery",
"post_no": 31899120,
"observed_at": "2026-09-29T02:04:00+09:00",
"status": "ok",
"views": 1875,
"up": null,
"down": null,
"comment_cnt": 21,
"rtb_vote": 0
}
],
"candidate_scores": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"snapshot_at": "2026-09-29T02:05:00+09:00",
"recommend_up_cnt": 3,
"recommend_up_cnt_m": 2,
"recommend_up_cnt_a": 1,
"comment_cnt": 2,
"comment_cnt_m": 1,
"comment_cnt_a": 0,
"hit_cnt": 5,
"hit_cnt_m": 3,
"hit_cnt_a": 1,
"dcbest_cnt": 0,
"dcbest_cnt_m": 0,
"dcbest_cnt_a": 0,
"memo_size": 1840,
"upimg_cnt": 2,
"upimg_height": 2410
}
],
"removals": [
{
"gall_id": "example_gallery",
"post_no": 31898050,
"reason": "hidden",
"decided_at": "2026-09-29T02:01:10+09:00"
}
]
}
{
"cycle_id": "20260929T0205",
"cycle_time": "2026-09-29T02:05:00+09:00",
"received_at": "2026-09-29T02:05:03+09:00",
"scored_at": "2026-09-29T02:05:09+09:00",
"model_version": "m0924-1",
"weights_version": "w-1",
"counts": {
"candidates": 1,
"engagement": 2,
"candidate_scores": 1,
"removals": 1,
"scored_now": 1,
"waiting": 1,
"not_scored": 0,
"results": 2
},
"missing_feature_share": 0.11,
"warnings": [],
"results": [
{
"gall_id": "example_gallery",
"post_no": 31899120,
"status": "scored",
"score": 0.62,
"p_views": 0.62,
"p_comments": 0.41,
"p_up": 0.37,
"downvote_share": 0.08,
"scored_at_cycle": "2026-09-29T02:05:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900001,
"status": "waiting",
"score": null,
"due_cycle": "2026-09-29T02:20:00+09:00"
}
]
}
{
"cycle_id": "20260929T0210",
"cycle_time": "2026-09-29T02:10:00+09:00",
"received_at": "2026-09-29T02:10:02+09:00",
"scored_at": "2026-09-29T02:10:03+09:00",
"model_version": "m0924-1",
"weights_version": "w-1",
"counts": {
"candidates": 1,
"engagement": 3,
"candidate_scores": 3,
"removals": 0,
"scored_now": 0,
"waiting": 1,
"not_scored": 1,
"results": 3
},
"missing_feature_share": 0.0,
"warnings": [
"candidates[0]: ignored unknown field \"channel\""
],
"results": [
{
"gall_id": "example_gallery",
"post_no": 31899120,
"status": "scored",
"score": 0.62,
"p_views": 0.62,
"p_comments": 0.41,
"p_up": 0.37,
"downvote_share": 0.08,
"scored_at_cycle": "2026-09-29T02:05:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900001,
"status": "waiting",
"score": null,
"due_cycle": "2026-09-29T02:20:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900044,
"status": "not_scored",
"score": null,
"reason": "missing_title"
}
]
}
{
"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"
}
]
}
}
{
"error": {
"code": "cycle_conflict",
"message": "cycle 20260929T0205 was already received with a different body"
}
}
Score a Cycle
One call per five-minute cycle. Sends candidates, engagement, candidate scores and removals, and returns a status and publish score for every pending candidate.
gzip -c cycle.json | curl --request POST \
--url "https://rtb.example.com/v1/cycles" \
--header "Authorization: <api-key>" \
--header "Content-Type: application/json" \
--header "Content-Encoding: gzip" \
--data-binary @-
import gzip, json
import requests
body = json.load(open("cycle.json"))
response = requests.post(
"https://rtb.example.com/v1/cycles",
headers={
"Authorization": "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(body).encode("utf-8")),
timeout=60,
)
scores = response.json()
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
const response = await fetch("https://rtb.example.com/v1/cycles", {
method: "POST",
headers: {
Authorization: "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
body: gzipSync(readFileSync("cycle.json")),
signal: AbortSignal.timeout(60_000),
});
const scores = await response.json();
{
"cycle_id": "20260929T0205",
"cycle_time": "2026-09-29T02:05:00+09:00",
"candidates": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"title": "Example original title",
"created_at": "2026-09-29T01:58:12+09:00",
"extracted_at": "2026-09-29T02:03:40+09:00",
"source": "keyword",
"manual_flag": 0,
"images_in_post": 2
}
],
"engagement": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"observed_at": "2026-09-29T02:04:00+09:00",
"status": "ok",
"views": 412,
"up": 9,
"down": 1,
"comment_cnt": 6,
"rtb_vote": 0
},
{
"gall_id": "example_gallery",
"post_no": 31899120,
"observed_at": "2026-09-29T02:04:00+09:00",
"status": "ok",
"views": 1875,
"up": null,
"down": null,
"comment_cnt": 21,
"rtb_vote": 0
}
],
"candidate_scores": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"snapshot_at": "2026-09-29T02:05:00+09:00",
"recommend_up_cnt": 3,
"recommend_up_cnt_m": 2,
"recommend_up_cnt_a": 1,
"comment_cnt": 2,
"comment_cnt_m": 1,
"comment_cnt_a": 0,
"hit_cnt": 5,
"hit_cnt_m": 3,
"hit_cnt_a": 1,
"dcbest_cnt": 0,
"dcbest_cnt_m": 0,
"dcbest_cnt_a": 0,
"memo_size": 1840,
"upimg_cnt": 2,
"upimg_height": 2410
}
],
"removals": [
{
"gall_id": "example_gallery",
"post_no": 31898050,
"reason": "hidden",
"decided_at": "2026-09-29T02:01:10+09:00"
}
]
}
{
"cycle_id": "20260929T0205",
"cycle_time": "2026-09-29T02:05:00+09:00",
"received_at": "2026-09-29T02:05:03+09:00",
"scored_at": "2026-09-29T02:05:09+09:00",
"model_version": "m0924-1",
"weights_version": "w-1",
"counts": {
"candidates": 1,
"engagement": 2,
"candidate_scores": 1,
"removals": 1,
"scored_now": 1,
"waiting": 1,
"not_scored": 0,
"results": 2
},
"missing_feature_share": 0.11,
"warnings": [],
"results": [
{
"gall_id": "example_gallery",
"post_no": 31899120,
"status": "scored",
"score": 0.62,
"p_views": 0.62,
"p_comments": 0.41,
"p_up": 0.37,
"downvote_share": 0.08,
"scored_at_cycle": "2026-09-29T02:05:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900001,
"status": "waiting",
"score": null,
"due_cycle": "2026-09-29T02:20:00+09:00"
}
]
}
{
"cycle_id": "20260929T0210",
"cycle_time": "2026-09-29T02:10:00+09:00",
"received_at": "2026-09-29T02:10:02+09:00",
"scored_at": "2026-09-29T02:10:03+09:00",
"model_version": "m0924-1",
"weights_version": "w-1",
"counts": {
"candidates": 1,
"engagement": 3,
"candidate_scores": 3,
"removals": 0,
"scored_now": 0,
"waiting": 1,
"not_scored": 1,
"results": 3
},
"missing_feature_share": 0.0,
"warnings": [
"candidates[0]: ignored unknown field \"channel\""
],
"results": [
{
"gall_id": "example_gallery",
"post_no": 31899120,
"status": "scored",
"score": 0.62,
"p_views": 0.62,
"p_comments": 0.41,
"p_up": 0.37,
"downvote_share": 0.08,
"scored_at_cycle": "2026-09-29T02:05:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900001,
"status": "waiting",
"score": null,
"due_cycle": "2026-09-29T02:20:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900044,
"status": "not_scored",
"score": null,
"reason": "missing_title"
}
]
}
{
"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"
}
]
}
}
{
"error": {
"code": "cycle_conflict",
"message": "cycle 20260929T0205 was already received with a different body"
}
}
Overview
The Score a Cycle API is the one call DCinside makes at every five-minute boundary. The request carries four arrays that mirror tables DCinside already delivers:candidates (the queue), engagement (the engagement series), candidate_scores (the admin candidate-score table) and removals (queue outcomes and deletions). The response has one row per pending candidate, with its status and publish score, inside an envelope of counts and versions.removals. Send each candidate once. Send engagement for every pending post in every cycle, because the growth features are built from that series.null for a counter you do not know. Never send 0 instead: the model treats a missing value differently from a zero.Cadence & timing
Cadence
:00, :05, :10 and so on, Korean time. cycle_time must be on that grid and not in the future. The interval is a setting on DCinside’s side, see Conventions.Timeout
GET /v1/scores/{cycle_id}.ceil5(max(extracted_at, first call that carries it) + 15 minutes). Until then it comes back as waiting. See How Scoring Works.Request Attributes
The body is gzip-compressed JSON. Its limits are 16 MB after decompression, 200candidates and 5,000 rows in each other array.YYYYMMDDTHHMM, for example 20260929T0205. The same id with the same body returns the stored response. The same id with a different body returns 409.Show item properties
Show item properties
post_no.gall_id and post_no together.queue.subject.queue.created_at.queue.extracted_at.unknown appears only in the delivered files (posts first captured after processing), so live posts should carry a real source. Delivered files: queue.source. One of score, setting, keyword, vote, manual, unknown.1 when the post was registered by hand from external monitoring. An input. Delivered files: queue.manual_flag. One of 0, 1.image_manifest.images_in_post (v1.3).images_in_post. In v1.3, 509 of 559,329 images (0.09%) came back empty or with a 403, so gaps happen. Delivered files: image_manifest.image_index (v1.3).queue.body_html.main, minor, mini.gallery_type: we read it from the registry, and you send it for galleries added since.removals.Show item properties
Show item properties
post_no.gall_id and post_no together.cycle_time. The model takes the latest row at or before the cycle, and its growth features need that row within 10 minutes of the cycle. Delivered files: engagement_ts.ts.ok = read normally, missing = the post is gone, nomap = the gallery could not be mapped. A categorical input. A post whose latest status is missing or nomap is not scored. One of ok, missing, nomap.up.Show item properties
Show item properties
post_no.gall_id and post_no together.candidate_score_ts.snapshot_ts.null.null.null.null.null.null.null.null.null.null.null.null.Show item properties
Show item properties
post_no.gall_id and post_no together.saved (placed on a board), hidden, deleted, or aged_out (over 24 hours: DCinside applies a 24-hour age filter and sends a removal signal when a pending candidate crosses it). It stops being scored and stops appearing in responses. Delivered files: queue.outcome, deletion. One of saved, hidden, deleted, aged_out.reason is saved, otherwise omit. One of main, light, night, app.queue.decided_at, deletion.deleted_at.saved or hidden decision. The code list is still being agreed. Surf proposed low_quality, stale, quota_full, duplicate, policy_violation and other for hides.Response Attributes
received_at is the scoring time.weights_version.Show properties
Show properties
candidates.engagement.candidate_scores.removals.waiting.not_scored.results: every pending post, including earlier scores repeated unchanged.Show item properties
Show item properties
post_no.gall_id and post_no together.scored = the post has a score. waiting = it is still inside its 15-minute wait. not_scored = it cannot be scored, see reason. One of scored, waiting, not_scored.w_v·P(views) + w_c·P(comments) + w_u·P(up) − w_d·downvote_share, with the weights in force. null unless status is scored.null unless scored.null unless scored.null unless scored.null unless scored.status is scored. A score is taken once and repeated unchanged in later cycles until the post is removed.status is waiting.status is not_scored. missing_title: the title is empty. missing_clock: created_at or extracted_at is missing. post_unavailable: the latest engagement status is missing or nomap. unknown_post: the post appears in engagement without a candidate record. One of missing_title, missing_clock, post_unavailable, unknown_post.gzip -c cycle.json | curl --request POST \
--url "https://rtb.example.com/v1/cycles" \
--header "Authorization: <api-key>" \
--header "Content-Type: application/json" \
--header "Content-Encoding: gzip" \
--data-binary @-
import gzip, json
import requests
body = json.load(open("cycle.json"))
response = requests.post(
"https://rtb.example.com/v1/cycles",
headers={
"Authorization": "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(body).encode("utf-8")),
timeout=60,
)
scores = response.json()
import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
const response = await fetch("https://rtb.example.com/v1/cycles", {
method: "POST",
headers: {
Authorization: "<api-key>",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
body: gzipSync(readFileSync("cycle.json")),
signal: AbortSignal.timeout(60_000),
});
const scores = await response.json();
{
"cycle_id": "20260929T0205",
"cycle_time": "2026-09-29T02:05:00+09:00",
"candidates": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"title": "Example original title",
"created_at": "2026-09-29T01:58:12+09:00",
"extracted_at": "2026-09-29T02:03:40+09:00",
"source": "keyword",
"manual_flag": 0,
"images_in_post": 2
}
],
"engagement": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"observed_at": "2026-09-29T02:04:00+09:00",
"status": "ok",
"views": 412,
"up": 9,
"down": 1,
"comment_cnt": 6,
"rtb_vote": 0
},
{
"gall_id": "example_gallery",
"post_no": 31899120,
"observed_at": "2026-09-29T02:04:00+09:00",
"status": "ok",
"views": 1875,
"up": null,
"down": null,
"comment_cnt": 21,
"rtb_vote": 0
}
],
"candidate_scores": [
{
"gall_id": "example_gallery",
"post_no": 31900001,
"snapshot_at": "2026-09-29T02:05:00+09:00",
"recommend_up_cnt": 3,
"recommend_up_cnt_m": 2,
"recommend_up_cnt_a": 1,
"comment_cnt": 2,
"comment_cnt_m": 1,
"comment_cnt_a": 0,
"hit_cnt": 5,
"hit_cnt_m": 3,
"hit_cnt_a": 1,
"dcbest_cnt": 0,
"dcbest_cnt_m": 0,
"dcbest_cnt_a": 0,
"memo_size": 1840,
"upimg_cnt": 2,
"upimg_height": 2410
}
],
"removals": [
{
"gall_id": "example_gallery",
"post_no": 31898050,
"reason": "hidden",
"decided_at": "2026-09-29T02:01:10+09:00"
}
]
}
{
"cycle_id": "20260929T0205",
"cycle_time": "2026-09-29T02:05:00+09:00",
"received_at": "2026-09-29T02:05:03+09:00",
"scored_at": "2026-09-29T02:05:09+09:00",
"model_version": "m0924-1",
"weights_version": "w-1",
"counts": {
"candidates": 1,
"engagement": 2,
"candidate_scores": 1,
"removals": 1,
"scored_now": 1,
"waiting": 1,
"not_scored": 0,
"results": 2
},
"missing_feature_share": 0.11,
"warnings": [],
"results": [
{
"gall_id": "example_gallery",
"post_no": 31899120,
"status": "scored",
"score": 0.62,
"p_views": 0.62,
"p_comments": 0.41,
"p_up": 0.37,
"downvote_share": 0.08,
"scored_at_cycle": "2026-09-29T02:05:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900001,
"status": "waiting",
"score": null,
"due_cycle": "2026-09-29T02:20:00+09:00"
}
]
}
{
"cycle_id": "20260929T0210",
"cycle_time": "2026-09-29T02:10:00+09:00",
"received_at": "2026-09-29T02:10:02+09:00",
"scored_at": "2026-09-29T02:10:03+09:00",
"model_version": "m0924-1",
"weights_version": "w-1",
"counts": {
"candidates": 1,
"engagement": 3,
"candidate_scores": 3,
"removals": 0,
"scored_now": 0,
"waiting": 1,
"not_scored": 1,
"results": 3
},
"missing_feature_share": 0.0,
"warnings": [
"candidates[0]: ignored unknown field \"channel\""
],
"results": [
{
"gall_id": "example_gallery",
"post_no": 31899120,
"status": "scored",
"score": 0.62,
"p_views": 0.62,
"p_comments": 0.41,
"p_up": 0.37,
"downvote_share": 0.08,
"scored_at_cycle": "2026-09-29T02:05:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900001,
"status": "waiting",
"score": null,
"due_cycle": "2026-09-29T02:20:00+09:00"
},
{
"gall_id": "example_gallery",
"post_no": 31900044,
"status": "not_scored",
"score": null,
"reason": "missing_title"
}
]
}
{
"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"
}
]
}
}
{
"error": {
"code": "cycle_conflict",
"message": "cycle 20260929T0205 was already received with a different body"
}
}
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.
gzip Body
Idempotency key: the cycle time in KST as YYYYMMDDTHHMM, for example 20260929T0205. The same id with the same body returns the stored response. The same id with a different body returns 409.
^\d{8}T\d{4}$"20260929T0205"
The five-minute boundary this call belongs to. Scores are taken as of this time, and the model's hour-of-day and day-of-week inputs are read from it in KST. A value off the grid or in the future gets 422.
^\d{4}-\d{2}-\d{2}T\d{2}:[0-5][05]:00\+09:00$"2026-09-29T02:05:00+09:00"
Posts that became candidates since the last successful call. Each post is sent once. May be empty.
200Show child attributes
Show child attributes
One row for every pending candidate, every cycle. Pending means sent in this or an earlier call and not yet in removals.
5000Show child attributes
Show child attributes
The admin candidate-score table's row for every pending candidate that has one. May be empty.
5000Show child attributes
Show child attributes
Posts leaving the pending pool since the last successful call. May be empty.
5000Show child attributes
Show child attributes
Response
The scores for this cycle.
Echoed from the request.
^\d{8}T\d{4}$"20260929T0205"
Echoed from the request.
^\d{4}-\d{2}-\d{2}T\d{2}:[0-5][05]:00\+09:00$"2026-09-29T02:05:00+09:00"
Our clock when the call arrived.
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+09:00$"2026-09-29T02:05:03+09:00"
Our clock when scoring finished. The gap from received_at is the scoring time.
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+09:00$"2026-09-29T02:05:09+09:00"
The model that produced the scores. Every score can be traced to it and to weights_version.
"m0924-1"
The publish-score weights in force for this cycle.
"w-1"
Row counts for this cycle.
Show child attributes
Show child attributes
Share of the model's 62 tabular inputs that were missing across the posts scored in this cycle. A jump means bad data upstream.
0 <= x <= 10.11
Extra fields we ignored, and anything else worth a look. Empty when clean.
[]
One row per pending candidate. Scores given earlier are repeated unchanged, so DCinside keeps no score memory of its own. Removed posts are not returned.
Show child attributes
Show child attributes