Submitter API Documentation

Summary

Submitter API is an API, where the Submitters can upload / manage their users videos in a easy and seamless maner.

The Submitter API uploads videos and model releases to your Glymt account and follows them through review to publication and sales. Files go straight to Glymt's storage through presigned URLs — no storage account of your own is needed, and nothing you upload is ever made public. API version 1.0; https only — http is refused.

Contact us

API clients are issued by Glymt on request. Glymt reviews the request and issues a client ID and secret.

Base URL

Every request of the Submitter API goes to this base URL. Only https is accepted: a request over http is refused.

Every answer is JSON with its own HTTP status. An unknown path under the base answers 404 with {"error": "not_found", "documentation": …}.

Base URL
https://api.glymt.com/upload/v1

Authentication

The Submitter API uses OAuth 2.0 client credentials (RFC 6749 §4.4). Glymt issues you a client ID and a client secret; the secret is shown once, when it is issued — keep it on your server. If it is lost or exposed, ask us for a new one: the old one stops working at once and every token issued with it is revoked.

Request a token from POST /oauth/token with grant_type=client_credentials, the client by HTTP Basic (client_id:client_secret) or by client_id and client_secret in the form body, never both. A user's username or password is never accepted.

Send the token on every other call: Authorization: Bearer {access_token}. A token lasts 7200 seconds (expires_in); then request a new one. A Submitter API token is refused by the Reseller API and the reverse (401 invalid_token).

Errors: 400 invalid_request / unsupported_grant_type, 401 invalid_client (with WWW-Authenticate) on the token request; 401 invalid_token on every other call.

POST /oauth/token
curl -s -X POST https://api.glymt.com/upload/v1/oauth/token \
  -u '{client_id}:{client_secret}' \
  -d grant_type=client_credentials
Response 200
{
  "access_token": "{access_token}",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "upload"
}
Every other call
curl -s https://api.glymt.com/upload/v1/account/summary \
  -H 'Authorization: Bearer {access_token}'

Endpoints

Every endpoint has its own path. Base URL https://api.glymt.com/upload/v1.

MethodPathEndpoint
GET / welcome (no token)
POST /oauth/token token
POST /batches/open open an upload batch
POST /batches/upload_url/:batch_id presigned upload URL for one file (video, release, csv)
POST /batches/submit/:batch_id submit the batch's metadata
GET /videos/list the submitter's videos and their state
GET /videos/status/:video_id one video's state (video id or submitter_video_id)
DELETE /videos/remove/:video_id remove a video
POST /releases/register register an uploaded model release
GET /releases/list the submitter's releases
GET /releases/details/:release_id one release with a 5-minute signed link
DELETE /releases/remove/:release_id remove a release
GET /account/summary statistics, read only
GET /account/sales sales with dates, paged

Uploading, step by step

  1. Open a batch — POST /batches/open answers a batch_id. Your upload quota is checked first (403 / 429 with reset_at).
  2. Ask for an upload URL per file — POST /batches/upload_url/:batch_id with the file's kind (video, release or csv), filename, size in bytes and content_type.
  3. PUT the file to the url you received, with the Content-Type header it names, within an hour. One PUT always suffices (a video is at most 2 GB).
  4. Register the model releases you uploaded — POST /releases/register — or name them in the rows of the next step.
  5. Submit the metadata — POST /batches/submit/:batch_id, JSON rows (or a CSV file of the batch). Each row answers accepted or refused with its reason.
  6. Follow each video — GET /videos/status/:video_id by your submitter_video_id, or GET /videos/list.

Filenames: letters, digits, -, _ and . only — no slash, no ... A video is a file the intake accepts, from 1 MB to 2 GB; a release is a PDF, JPEG or PNG of at most 20 MB; a CSV file at most 5 MB.

A worked example: one video, one release
curl -s -X POST https://api.glymt.com/upload/v1/batches/open \
  -H 'Authorization: Bearer {access_token}'

curl -s -X POST https://api.glymt.com/upload/v1/batches/upload_url/{batch_id} \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{ "kind": "video", "filename": "beach_sunset_01.mp4", "size": 734003200, "content_type": "video/mp4" }'

curl -s -X PUT '{presigned_upload_url}' \
  -H 'Content-Type: video/mp4' \
  --upload-file beach_sunset_01.mp4

curl -s -X POST https://api.glymt.com/upload/v1/batches/upload_url/{batch_id} \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{ "kind": "release", "filename": "release_ana.pdf", "size": 182044, "content_type": "application/pdf" }'

curl -s -X PUT '{presigned_upload_url}' -H 'Content-Type: application/pdf' --upload-file release_ana.pdf

curl -s -X POST https://api.glymt.com/upload/v1/batches/submit/{batch_id} \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{
        "videos": [
          {
            "file": "beach_sunset_01.mp4",
            "title": "Sunset over the beach",
            "description": "Golden hour on a quiet beach",
            "keywords": ["beach", "sunset", "sea", "waves"],
            "categories": ["Nature", "Travel"],
            "is_ai": false,
            "submitter_video_id": "SV-0001",
            "releases": ["release_ana.pdf"],
            "publish": true
          }
        ]
      }'

The CSV alternative

Instead of JSON rows, upload a CSV file to the batch (kind csv) and submit {"csv": "<its filename>"}. The first row is the header; the columns, in this order:

#ColumnContent
1filethe video's filename in the batch
2titleat most 200 characters
3keywordsseparated by ;
4releasesrelease filenames in the batch, or ids of releases already registered, separated by ;
5submitter_video_idyour own id for the video (letters, digits, - _ . ; at most 64)
6is_aitrue | false
7descriptionat most 5,000 characters
8categoriesGlymt category names, separated by ;
9publishtrue | false (empty: true) — published by itself once checked and approved, or left unpublished

Download the template: submitter_api_template.csv (file,title,keywords,releases,submitter_video_id,is_ai,description,categories,publish).

Submit a CSV file
curl -s -X POST https://api.glymt.com/upload/v1/batches/submit/{batch_id} \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{ "csv": "batch_metadata.csv" }'

Objects and statuses

Upload (a submitted video)
FieldTypeMeaning
submitter_video_idstring or nullyour id for it
video_id, token, titleinteger, string, string — or nullthe video, once the intake created it
statusstringqueued, processing, awaiting_approval, published, rejected, ignored, error
reasonstring or nullwhy it was rejected, ignored or refused
lifecycle_statestring or nullthe video's state on Glymt
submitted_at, published_atISO 8601 UTCwhen you submitted it and when it was published
assetsobject or nullonce processed: the cover (small, medium, large) and the unbranded previews, 570×320 (preview_sd) and 1920×1080 (preview_hd) — absolute https URLs; null before
sales_countintegerits licences
Upload
{
  "submitter_video_id": "SV-0001",
  "video_id": {video_id},
  "token": "{video_token}",
  "title": "Sunset over the beach",
  "status": "published",
  "reason": null,
  "lifecycle_state": "{lifecycle_state}",
  "submitted_at": "2026-10-30T12:00:00Z",
  "published_at": "2026-10-31T09:00:00Z",
  "assets": {
    "cover": {
      "small": "https://glymt.com/{cover_small_path}",
      "medium": "https://glymt.com/{cover_medium_path}",
      "large": "https://glymt.com/{cover_large_path}"
    },
    "preview_sd": "https://glymt.com/uploads/video/{video_id}/unbranded_sd.mp4",
    "preview_hd": "https://glymt.com/uploads/video/{video_id}/unbranded_hd.mp4"
  },
  "sales_count": 0
}
statusMeaning
queuedsubmitted; the intake has not created the video yet
processingthe video exists and is being processed
awaiting_approvalprocessed; in Glymt's review
publishedlive on Glymt
rejectedrefused by the review, or removed (with the reason)
ignored, errorthe intake refused the file (with the reason)
Release

{id, name, legal_age (adult | minor), videos_count, created_at}; its details add a signed url that lasts 300 seconds. Model releases are identity documents: never public.

GET/

The API's name, version and documentation. No token. No parameters.

Request
curl -s https://api.glymt.com/upload/v1
Response 200
{
  "name": "Glymt Submitter API",
  "version": "1.0",
  "documentation": "https://glymt.com/submitter_api_documentation",
  "description": "{summary}"
}

POST/oauth/token

A Bearer token for 7200 seconds — see Authentication.

Errors: 400 invalid_request, 400 unsupported_grant_type, 401 invalid_client, 429.

Request
curl -s -X POST https://api.glymt.com/upload/v1/oauth/token \
  -u '{client_id}:{client_secret}' \
  -d grant_type=client_credentials
Response 200
{
  "access_token": "{access_token}",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "upload"
}

POST/batches/open

Opens a batch: 201 {batch_id, upload_url, submit}. No parameters. Your quota is checked first.

Errors: 400 invalid_request, 401 invalid_token, 403 (the quota refuses uploads now, with its reason), 429 monthly_ceiling (with reset_at).

Request
curl -s -X POST https://api.glymt.com/upload/v1/batches/open \
  -H 'Authorization: Bearer {access_token}'
Response 201
HTTP/1.1 201 Created

{
  "batch_id": "{batch_id}",
  "upload_url": "/upload/v1/batches/upload_url/{batch_id}",
  "submit": "/upload/v1/batches/submit/{batch_id}"
}
Response 429
HTTP/1.1 429 Too Many Requests

{ "error": "monthly_ceiling", "reset_at": "2026-11-01T00:00:00Z" }

POST/batches/upload_url/:batch_id

A presigned PUT URL (one hour) for one file of the batch. Body: kind (video | release | csv), filename, size (bytes), content_type (a video: video/*; a release: application/pdf, image/jpeg or image/png; a CSV: text/csv). Send the PUT with exactly the Content-Type the answer names.

Errors: 400 invalid_request (a kind, filename, size or type out of its limits), 401 invalid_token, 404 not_found (a batch that was not opened), 422 (a video the intake would refuse: its size or format).

Request
curl -s -X POST https://api.glymt.com/upload/v1/batches/upload_url/{batch_id} \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{ "kind": "video", "filename": "beach_sunset_01.mp4", "size": 734003200, "content_type": "video/mp4" }'
Response 200
{
  "batch_id": "{batch_id}",
  "kind": "video",
  "file": "beach_sunset_01.mp4",
  "method": "PUT",
  "url": "{presigned_upload_url}",
  "headers": { "Content-Type": "video/mp4" },
  "expires_in": 3600
}
The upload
curl -s -X PUT '{presigned_upload_url}' \
  -H 'Content-Type: video/mp4' \
  --upload-file beach_sunset_01.mp4

POST/batches/submit/:batch_id

The batch's metadata: JSON {"videos": [row, …]} (1 – 500 rows) or {"csv": "<filename>"}. A row: file (required, uploaded to the batch), title, description, keywords (list), categories (Glymt category names), is_ai, submitter_video_id (unique per account), releases (release filenames of the batch or ids of registered releases), publish (default true).

publish true (or omitted): the video is published by itself once it passes our checks (and, for a new account, our approval) and goes straight to our automated review and curation. false: it waits unpublished in your account; open it on its edit page on Glymt to finish it and publish it.

publish true (or omitted): the video is published by itself once it passes our checks (and, for a new account, our approval) and goes straight to our automated review and curation. false: it waits unpublished in your account; open it on its edit page on Glymt to finish it and publish it.

publish true (or omitted): the video is published by itself once it passes our checks (and, for a new account, our approval) and goes straight to our automated review and curation. false: it waits unpublished in your account; open it on its edit page on Glymt to finish it and publish it.

Every row is checked against the batch and admitted by your quota, then handed to Glymt's intake. The answer lists every row (accepted or refused with its reason). When the quota refuses a row, the batch stops there: that row and the ones after it are refused, the rows before stay accepted, and the answer is 403 or 429 with reset_at.

Errors: 400 invalid_request (malformed body), 401 invalid_token, 403 / 429 (the quota), 404 not_found (the batch).

Request
curl -s -X POST https://api.glymt.com/upload/v1/batches/submit/{batch_id} \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{
        "videos": [
          {
            "file": "beach_sunset_01.mp4",
            "title": "Sunset over the beach",
            "description": "Golden hour on a quiet beach",
            "keywords": ["beach", "sunset", "sea", "waves"],
            "categories": ["Nature", "Travel"],
            "is_ai": false,
            "submitter_video_id": "SV-0001",
            "releases": ["release_ana.pdf"],
            "publish": true
          }
        ]
      }'
Response 200
{
  "batch_id": "{batch_id}",
  "accepted": 1,
  "refused": 1,
  "rows": [
    { "submitter_video_id": "SV-0001", "file": "beach_sunset_01.mp4", "result": "accepted", "reason": null, "submitted_video_id": {submission_id} },
    { "submitter_video_id": "SV-0002", "file": "city_02.mp4", "result": "refused", "reason": "file city_02.mp4 is not in the batch (upload it to its upload_url first)", "submitted_video_id": null }
  ]
}

GET/videos/list

Your submitted videos, newest first: page (from 1), per_page (1 – 100, default 50).

Errors: 400 invalid_request, 401 invalid_token.

Request
curl -s 'https://api.glymt.com/upload/v1/videos/list?page=1&per_page=50' \
  -H 'Authorization: Bearer {access_token}'
Response 200
{
  "count": {submissions},
  "page": 1,
  "per_page": 50,
  "videos": [ {Upload}, … ]
}

GET/videos/status/:video_id

One video by your submitter_video_id or its Glymt video id: {video: Upload}.

Errors: 400 invalid_request, 401 invalid_token, 404 not_found.

Request
curl -s https://api.glymt.com/upload/v1/videos/status/SV-0001 \
  -H 'Authorization: Bearer {access_token}'
Response 200
{ "video": {Upload} }

DELETE/videos/remove/:video_id

Removes one of your videos (by submitter_video_id or video id): 202.

Errors: 400 invalid_request, 401 invalid_token, 404 not_found, 409 not_a_video_yet (still queued), 423 locked (a lock-in holds the video).

Request
curl -s -X DELETE https://api.glymt.com/upload/v1/videos/remove/{video_id} \
  -H 'Authorization: Bearer {access_token}'
Response 202
HTTP/1.1 202 Accepted

{ "video_id": {video_id}, "state": "removing" }

POST/releases/register

Registers a model release uploaded to a batch (kind release). Body: batch_id, file, name (1 – 120 characters), legal_age (adult | minor). 202 while it registers; the same file again answers 200 with the existing release.

Errors: 400 invalid_request, 401 invalid_token, 404 not_found (the file is not in the batch).

Request
curl -s -X POST https://api.glymt.com/upload/v1/releases/register \
  -H 'Authorization: Bearer {access_token}' \
  -H 'Content-Type: application/json' \
  -d '{ "batch_id": "{batch_id}", "file": "release_ana.pdf", "name": "Ana", "legal_age": "adult" }'
Response 202
HTTP/1.1 202 Accepted

{ "name": "Ana", "file": "release_ana.pdf", "state": "registering", "list": "/upload/v1/releases/list" }

GET/releases/list

Your releases (metadata only): page, per_page (1 – 100, default 50).

Errors: 400 invalid_request, 401 invalid_token.

Request
curl -s 'https://api.glymt.com/upload/v1/releases/list?page=1&per_page=50' \
  -H 'Authorization: Bearer {access_token}'
Response 200
{
  "count": 1,
  "page": 1,
  "per_page": 50,
  "releases": [
    { "id": {release_id}, "name": "Ana", "legal_age": "adult", "videos_count": 1, "created_at": "2026-10-30T12:00:00Z" }
  ]
}

GET/releases/details/:release_id

One release and a signed link to its file that lasts 300 seconds.

Errors: 400 invalid_request, 401 invalid_token, 404 not_found.

Request
curl -s https://api.glymt.com/upload/v1/releases/details/{release_id} \
  -H 'Authorization: Bearer {access_token}'
Response 200
{
  "release": {
    "id": {release_id}, "name": "Ana", "legal_age": "adult", "videos_count": 1, "created_at": "2026-10-30T12:00:00Z",
    "url": "{signed_release_url}", "expires_in": 300
  }
}

DELETE/releases/remove/:release_id

Removes one of your releases: 202.

Errors: 400 invalid_request, 401 invalid_token, 404 not_found.

Request
curl -s -X DELETE https://api.glymt.com/upload/v1/releases/remove/{release_id} \
  -H 'Authorization: Bearer {access_token}'
Response 202
HTTP/1.1 202 Accepted

{ "release_id": {release_id}, "state": "removing" }

GET/account/summary

Read only: your client, your videos by status, published and sales counts, your earnings and unpaid earnings, and your upload quota (tier, this month admitted and its ceiling, the reset, the lifetime allowance). No parameters.

Errors: 400 invalid_request, 401 invalid_token.

Request
curl -s https://api.glymt.com/upload/v1/account/summary \
  -H 'Authorization: Bearer {access_token}'
Response 200
{
  "client": { "name": "{client_name}", "client_id": "{client_id}" },
  "videos": { "submitted": 12, "processing": 1, "awaiting_approval": 2, "published": 8, "rejected": 1, "removed": 0 },
  "published_count": 8,
  "sales_count": 3,
  "earnings": 21.5,
  "unpaid_earnings": 7.25,
  "quota": {
    "tier": "{tier}",
    "month_admitted": 12,
    "monthly_ceiling": {monthly_ceiling},
    "resets_at": "2026-11-01T00:00:00Z",
    "lifetime_admitted": 12,
    "lifetime_allowance": {lifetime_allowance}
  }
}

GET/account/sales

Your sales, newest first: page, per_page (1 – 100, default 50), and optionally start_date with end_date (YYYY-MM-DD, together). Each sale: video_id, sold_at, channel — which videos sold and when; your totals are in /account/summary.

Errors: 400 invalid_request, 401 invalid_token.

Request
curl -s 'https://api.glymt.com/upload/v1/account/sales?page=1&start_date=2026-10-01&end_date=2026-10-31' \
  -H 'Authorization: Bearer {access_token}'
Response 200
{
  "count": 1,
  "page": 1,
  "per_page": 50,
  "sales": [
    { "video_id": {video_id}, "sold_at": "2026-10-20T15:00:00Z", "channel": "Glymt" }
  ]
}

Errors

Every error is JSON with its HTTP status — {"error": code, "message": text}, or the OAuth form {"error": code, "error_description": text} for the token and the Bearer checks. Nothing answers 200 with an error.

Error body
{ "error": "invalid_request", "message": "unknown parameter(s): bucket" }
StatuserrorWhen
400invalid_request, unsupported_grant_typea missing, unknown or malformed parameter; a malformed body
401invalid_clientthe token request: unknown client, wrong secret, inactive client or owner
401invalid_tokenno token, or one unknown, expired, revoked or issued for the Reseller API
402—not answered by the Submitter API
403the quota's reasonthe upload quota refuses uploads now
404not_foundan unknown path, batch, video or release
409not_a_video_yetremoving a video the intake has not created yet
413—a request body above the size limit, refused before it reaches the API (files go by their presigned URL)
415—not answered by v1: a body that is neither JSON nor form-encoded is not read, and the request answers 400
422the intake's reasonan upload URL for a video the intake would refuse (size or format)
423lockedremoving a video a lock-in holds
429monthly_ceiling, too_many_requeststhe monthly upload ceiling (with reset_at), or a rate limit (with Retry-After)
500server_erroran error on Glymt's side; retry later
404
{ "error": "not_found", "documentation": "https://glymt.com/submitter_api_documentation" }

Limits

  • Token requests: 10 a minute and 100 an hour per client, 20 a minute per IP address.
  • Every other call: 600 a minute per token — then 429 with Retry-After (seconds).
  • Uploads: your account's upload quota (/account/summary); a batch holds up to 500 rows; an upload URL lasts one hour.

Changelog

API version 1.0.

  • 1.0 — OAuth 2.0 client credentials (client ID and secret; Bearer tokens of 7200 seconds, one per API); every endpoint at its own path (the table above); uploads by presigned URL — no storage account needed, nothing made public; JSON metadata with per-row results (CSV still accepted); statuses and sales read only. The earlier login with a username and password and the earlier /videos, /releases and /users endpoints are removed.
admin login browse the video dataset site administration php login page configuration file