Files

406 lines
17 KiB
HTML
Raw Permalink Normal View History

2026-05-23 17:17:56 -07:00
<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>