406 lines
17 KiB
HTML
406 lines
17 KiB
HTML
|
|
<div class="document-section">
|
||
|
|
<p class="document-header">API Guide</p>
|
||
|
|
<p class="document-paragraph">Last Updated: April 13th 2026</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<div class="document-section">
|
||
|
|
<p class="document-header">[ Introduction ]</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
The gifuu API is publicly accessible and requires no authentication
|
||
|
|
for read operations. We recommend that you have the client make requests
|
||
|
|
on their own instead of proxying requests for them to avoid issues with
|
||
|
|
our rate limits.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<p>Use the following URLs for HTTP requests:</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
https://api.gifuu.pancakz.net/ // Base URL for API Requests
|
||
|
|
https://cdn.gifuu.pancakz.net/ // Base URL for CDN Requests
|
||
|
|
|
||
|
|
https://cdn.gifuu.pancakz.net/{id}/preview.avif // Up to 240px at 16fps
|
||
|
|
https://cdn.gifuu.pancakz.net/{id}/standard.avif // Up to 720px at 60fps
|
||
|
|
https://cdn.gifuu.pancakz.net/{id}/alpha.webm // See "Transparency" section below
|
||
|
|
https://cdn.gifuu.pancakz.net/{id}/standard.ogg // See "Audio" section below
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<p>Ratelimit headers are provided with each request:</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
X-Ratelimit-Category // Endpoint Category (a.k.a Bucket)
|
||
|
|
X-Ratelimit-Reset // Seconds until reset (float string)
|
||
|
|
X-Ratelimit-Limit // Requests allowed per period
|
||
|
|
X-Ratelimit-Remaining // Requests left before 429 errors appear
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-divider"></div>
|
||
|
|
|
||
|
|
<p class="document-header">[ Transparency ]</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
gifuu stores animations (also referred to as Art) as AVIF a very new and modern format.
|
||
|
|
We sacrifice compatibility with older devices to gain massive efficiency in file size and visual quality.
|
||
|
|
Unfortunately AVIF doesn't natively support transparency, so we use a stacked video technique to encode
|
||
|
|
the alpha channel (transparency) alongside the color data.
|
||
|
|
</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
The <code>alpha.webm</code> file is a double-height video where the top half contains the
|
||
|
|
color data and the bottom half contains the alpha channel encoded as a grayscale luma
|
||
|
|
(brightness) map. White pixels signify opaque, black pixels signify transparent.
|
||
|
|
</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
To render this correctly in the browser you must use a WebGL fragment shader to composite
|
||
|
|
the two halves together. The following shader can be used as a reference:
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
// Sample color from top half, alpha from bottom half
|
||
|
|
vec2 colorUV = vec2(vUV.x, vUV.y * 0.5);
|
||
|
|
vec2 alphaUV = vec2(vUV.x, 0.5 + vUV.y * 0.5);
|
||
|
|
|
||
|
|
// Apply colors to a texture
|
||
|
|
vec4 color = texture2D(uFrame, colorUV);
|
||
|
|
float alpha = texture2D(uFrame, alphaUV).r;
|
||
|
|
gl_FragColor = vec4(color.rgb, alpha);
|
||
|
|
</pre>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
This technique was pioneered by
|
||
|
|
<a target="_blank" href="https://jakearchibald.com/2024/video-with-transparency/">Jake Archibald</a>.
|
||
|
|
If you don't want to implement this yourself, you can embed our player by clicking the
|
||
|
|
<code>EMBED</code> button while viewing any animation.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<p class="document-header">[ Audio ]</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
You can check for audio by reading the <code>audio</code> field on Art objects, or by
|
||
|
|
requesting it from the CDN and checking for a <code>404 Not Found</code> response.
|
||
|
|
</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Content is encoded with the <a target="_blank" href="https://en.wikipedia.org/wiki/Opus_(audio_format)">Opus</a>
|
||
|
|
codec inside an <a target="_blank" href="https://en.wikipedia.org/wiki/Ogg">Ogg</a> container
|
||
|
|
for compatibility with Apple devices.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div class="document-divider"></div>
|
||
|
|
|
||
|
|
<p class="document-header">[ Object Types ]</p>
|
||
|
|
<p class="document-paragraph">The following types are returned as responses across API endpoints:</p>
|
||
|
|
|
||
|
|
<p class="document-subheader">Object: Tag</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
{
|
||
|
|
"id": string // Tag ID (snowflake string)
|
||
|
|
"label": string // Tag Name
|
||
|
|
"usage": number // Number of animations using this tag
|
||
|
|
}
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<p class="document-subheader">Object: Art</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
{
|
||
|
|
"id": string // Animation ID (snowflake string)
|
||
|
|
"created": string // ISO 8601 Timestamp
|
||
|
|
"sticker": boolean // Is Static?
|
||
|
|
"audio": boolean // Has Audio?
|
||
|
|
"framerate": number // Approximate Framerate
|
||
|
|
"width": number // Approximate Width
|
||
|
|
"height": number // Approximate Height
|
||
|
|
"rating": string // NSFW Rating (string float, range: 0.0 - 1.0)
|
||
|
|
"title": string // Associated Title
|
||
|
|
"tags": Tag[] // Associated Tags
|
||
|
|
}
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-divider"></div>
|
||
|
|
|
||
|
|
<p class="document-header">[ Special ]</p>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /limits</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Returns upload constraints and validation rules for all user input fields.
|
||
|
|
The regex patterns and normalizer rules here are authoritative.
|
||
|
|
You must sanitize your inputs against them before submitting or the server will reject your request.
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Response Body:
|
||
|
|
{
|
||
|
|
"upload": {
|
||
|
|
"input_width_min": number // Minimum input width (64px)
|
||
|
|
"input_height_min": number // Minimum input height (64px)
|
||
|
|
"video_width_max": number // Maximum video width (3840px)
|
||
|
|
"video_height_max": number // Maximum video height (2160px)
|
||
|
|
"image_width_max": number // Maximum image width (7680px)
|
||
|
|
"image_height_max": number // Maximum image height (4320px)
|
||
|
|
"duration": number // Maximum duration in seconds (62s)
|
||
|
|
"filesize": number // Maximum file size in bytes
|
||
|
|
"mime_types": string[] // Accepted MIME types
|
||
|
|
}
|
||
|
|
"title": {
|
||
|
|
"normalizers": NormalizerRule[] // Apply before validating
|
||
|
|
"matcher": string // Regex pattern
|
||
|
|
"max_length": number // 80
|
||
|
|
"min_length": number // 1
|
||
|
|
}
|
||
|
|
"tag": {
|
||
|
|
"normalizers": NormalizerRule[]
|
||
|
|
"matcher": string // ^[\p{L}\p{N}_]{1,32}$
|
||
|
|
"max_length": number // 32
|
||
|
|
"min_length": number // 1
|
||
|
|
}
|
||
|
|
"comment": {
|
||
|
|
"normalizers": NormalizerRule[]
|
||
|
|
"matcher": string
|
||
|
|
"max_length": number // 240
|
||
|
|
"min_length": number // 10
|
||
|
|
}
|
||
|
|
"report": {
|
||
|
|
"values": [ // Valid report reason types
|
||
|
|
{ "id": number, "title": string, "description": string }
|
||
|
|
]
|
||
|
|
"normalizers": NormalizerRule[]
|
||
|
|
"matcher": string
|
||
|
|
"max_length": number // 240
|
||
|
|
"min_length": number // 10
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Object: NormalizerRule
|
||
|
|
{
|
||
|
|
"match": string // Regex pattern to find
|
||
|
|
"replace": string // Replacement string
|
||
|
|
"comment": string // Human-readable description
|
||
|
|
}
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /challenge</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Returns a fresh Proof of Work challenge.
|
||
|
|
You select the difficulty, the server enforces a minimum of <code>18</code>.
|
||
|
|
Challenges expire after 5 minutes.
|
||
|
|
They are consumed immediately upon use even if the request fails.
|
||
|
|
</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Provide your completed counter and given nonce to endpoints that require PoW via the
|
||
|
|
<code>X-Pow-Counter</code> and <code>X-Pow-Nonce</code> headers respectively.
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Query Parameters:
|
||
|
|
difficulty number // Desired difficulty (minimum: 18)
|
||
|
|
|
||
|
|
Response Body:
|
||
|
|
{
|
||
|
|
"nonce": string // Hex-encoded nonce
|
||
|
|
"difficulty": number // Confirmed difficulty
|
||
|
|
"expires": number // Expiry as UNIX timestamp
|
||
|
|
}
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Some endpoints enforce a higher minimum difficulty than the global floor.
|
||
|
|
Request at least the required difficulty for the endpoint you intend to call or it will be rejected:
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Endpoint Minimum Difficulty
|
||
|
|
------------------------ ------------------
|
||
|
|
POST /uploads 20
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<p class="document-paragraph">Example Solver (JavaScript):</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
const { nonce, difficulty } = await fetch("/challenge?difficulty=18").then(r => r.json())
|
||
|
|
const encoder = new TextEncoder()
|
||
|
|
let counter = 0
|
||
|
|
while (true) {
|
||
|
|
const data = encoder.encode(nonce + counter)
|
||
|
|
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", data))
|
||
|
|
let zeroBits = 0
|
||
|
|
for (const byte of hash) {
|
||
|
|
if (byte === 0) { zeroBits += 8 }
|
||
|
|
else { zeroBits += Math.clz32(byte) - 24; break }
|
||
|
|
}
|
||
|
|
if (zeroBits >= difficulty) break
|
||
|
|
counter++
|
||
|
|
}
|
||
|
|
|
||
|
|
// Submit nonce + counter with your upload via X-Pow-Nonce and X-Pow-Counter headers
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-divider"></div>
|
||
|
|
|
||
|
|
<p class="document-header">[ Tags ]</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
gifuu uses tags to make its database queryable. Sanitize tag strings against the rules
|
||
|
|
provided by <code>limits.tags</code> or the server will reject your request.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /tags/popular</p>
|
||
|
|
<p class="document-paragraph">Returns the most popular tags (highest usage) on the platform.</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Query Parameters:
|
||
|
|
limit number // Amount of results to return (range 1-100)
|
||
|
|
|
||
|
|
Response Body:
|
||
|
|
Tag[]
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /tags/autocomplete</p>
|
||
|
|
<p class="document-paragraph">Search for tags with a similar spelling using word similarity ranking.</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Query Parameters:
|
||
|
|
query string // Search query — must pass validation rules from 'limits.tag'
|
||
|
|
limit number // Amount of results to return (range 1-100)
|
||
|
|
|
||
|
|
Response Body:
|
||
|
|
Tag[]
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-divider"></div>
|
||
|
|
|
||
|
|
<p class="document-header">[ Art ]</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Content is processed and served as AVIF files for efficiency. Most modern web browsers
|
||
|
|
and operating systems <a target="_blank" href="https://caniuse.com/avif">support this format</a>.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /art/latest</p>
|
||
|
|
<p class="document-paragraph">Returns the most recently uploaded animations, newest first.</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Query Parameters:
|
||
|
|
limit number // Amount of results to return (range 1-100)
|
||
|
|
after string? // Cursor for pagination — snowflake ID of last seen item (optional)
|
||
|
|
|
||
|
|
Response Body:
|
||
|
|
Art[]
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /art/search</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Returns animations matching all provided tags (AND logic). At least one
|
||
|
|
<code>tag</code> parameter is required.
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Query Parameters:
|
||
|
|
tag string // Tag ID to filter by (snowflake string) — repeat for multiple tags
|
||
|
|
limit number // Amount of results to return (range 1-100)
|
||
|
|
after string? // Cursor for pagination — snowflake ID of last seen item (optional)
|
||
|
|
|
||
|
|
// Example: /art/search?tag=123&tag=456&limit=20
|
||
|
|
|
||
|
|
Response Body:
|
||
|
|
Art[]
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">GET /art/{id}</p>
|
||
|
|
<p class="document-paragraph">Returns metadata for a single animation.</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Response Body:
|
||
|
|
Art
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">DELETE /art/{id}</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Deletes an animation. Requires the edit token returned at upload time,
|
||
|
|
passed as a query parameter. Responds with <code>204 No Content</code> on success.
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Query Parameters:
|
||
|
|
token string // Edit token from upload response
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">POST /art/{id}/reports</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Submits a moderation report for an animation.
|
||
|
|
Valid reason type IDs are listed in <code>limits.report.values</code></code>.
|
||
|
|
The reason text must pass the <code>report</code> validation rules from the same endpoint.
|
||
|
|
Responds with <code>204 No Content</code> on success.
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Request Body:
|
||
|
|
{
|
||
|
|
"type": number // Report reason ID (see 'limits.report.values')
|
||
|
|
"reason": string // Description of the issue (10-240 characters)
|
||
|
|
}
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<div class="document-divider"></div>
|
||
|
|
|
||
|
|
<p class="document-header">[ Uploads ]</p>
|
||
|
|
<p class="document-paragraph">Endpoints for creating and monitoring uploads.</p>
|
||
|
|
|
||
|
|
<div class="document-spacer"></div>
|
||
|
|
|
||
|
|
<p class="document-subheader">POST /uploads</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
Uploads an animation to the site. To prevent spam this endpoint requires a valid
|
||
|
|
Proof of Work challenge solved via <code>GET /challenge</code> with a minimum difficulty of <code>20</code>.
|
||
|
|
</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
This endpoint responds as a <strong>Server-Sent Events (SSE) stream</strong>. Events are
|
||
|
|
emitted throughout processing to report progress. The connection closes after the final
|
||
|
|
<code>finish</code> event or on any error.
|
||
|
|
</p>
|
||
|
|
<p class="document-paragraph">
|
||
|
|
NOTE: Requests exceeding the <code>limits.upload.filesize</code> limit will be aborted immediately.
|
||
|
|
</p>
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
Request Body: [multipart/form-data]
|
||
|
|
|
||
|
|
field: data (text/JSON)
|
||
|
|
{
|
||
|
|
"title": string // Animation title (1-80 characters, see 'limits.title')
|
||
|
|
"tags": string[] // Tag names to attach, plaintext (see 'limits.tag')
|
||
|
|
}
|
||
|
|
|
||
|
|
field: file (binary)
|
||
|
|
// Accepted MIME types may change, fetch the current list from 'limits.upload.mime_types'
|
||
|
|
image/jpeg, image/png, image/gif, image/webp, image/heic, image/heif,
|
||
|
|
image/avif, image/jxl, image/tiff, image/bmp,
|
||
|
|
video/mp4, video/webm, video/quicktime, video/x-matroska,
|
||
|
|
video/avi, video/x-ms-wmv,
|
||
|
|
|
||
|
|
Request Headers:
|
||
|
|
X-Pow-Nonce // Nonce from GET /challenge
|
||
|
|
X-Pow-Counter // Your solved counter value
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
<pre class="document-codeblock">
|
||
|
|
SSE Event Stream:
|
||
|
|
|
||
|
|
event: id // Emitted early — the assigned snowflake ID for this upload
|
||
|
|
{ "id": string }
|
||
|
|
|
||
|
|
event: step // Processing stage updates
|
||
|
|
{ "id": string, "message": string }
|
||
|
|
// Known step IDs: PROBE_QUEUE, PROBE_START, SERVER_FINALIZE
|
||
|
|
|
||
|
|
event: progress // Encoding/classification progress
|
||
|
|
{ "percent": string } // Float string, e.g. "42.50"
|
||
|
|
|
||
|
|
event: finish // Final event on success — save edit_token, it is not recoverable!
|
||
|
|
{
|
||
|
|
"id": string // Animation snowflake ID
|
||
|
|
"edit_token": string // Required to delete this animation later
|
||
|
|
}
|
||
|
|
|
||
|
|
event: error // Emitted on client or server error, stream closes after
|
||
|
|
{ "code": number, "message": string }
|
||
|
|
</pre>
|
||
|
|
|
||
|
|
</div>
|