Reference: Service, Sign-in and Account

Whether the service is up, signing in and out, signing up, and who is signed in with what they can order.

Service

Two endpoints that need no sign-in: whether the service is up, and the machine-readable description of the API.

GET /health #

Credential: none - send no Authorization header

Tells you that the service is up, whether its database answers, which API version it runs, which optional features are switched on, and the website's addresses. Use it to test your connection before anything else - it needs no key.

Parameters: none. Send the request exactly as in the example.

What you get back: 200 OK with: status (ok, or degraded while the database does not answer); database (ok or down: whether our database answered a short query - asked at most every 10 seconds); api_version (1.0); site_build (the website's version); time (the server's clock, UTC); min_client_versions (per app id the lowest version still accepted - an older app gets ClientVersionUnsupported; empty: none is refused); features (true or false per optional feature: app_logins and browser_login - signing in from an app, signed_result_links - POST /jobs/{job_id}/result-links, webhooks, signup - POST /auth/signup, email_code_login - POST /auth/login/code, web_links - POST /auth/web-links); limits.picture (the largest picture we take: max_long_side and max_short_side in pixels, either way round - check a file against it before you upload; see Billing); pricing (our list prices: tiers from small to large, each with name, max_long_side, max_short_side and eur_per_min, and output_4k_eur_per_min, the price per minute of a job that delivers 4K); links (website pages an app shows before anyone signs in: the website; signup and pricing, both the start page, whose Sign Up opens the sign-up form - a signed-in account sees its own prices in GET /me; terms; privacy; support - the Contact Us part of the About Us page; this page).

Good to know: The answer can be up to 60 seconds old (Cache-Control: public, max-age=60); while the database does not answer it is not kept (no-store). It is 200 OK in both cases - check database. No Recuro-Client header is needed.

Request
curl -sS "$API/health"
Response: 200 OK
{
  "status": "ok",
  "database": "ok",
  "api_version": "1.0",
  "site_build": "5.11.0",
  "time": "2026-10-06T09:03:38Z",
  "min_client_versions": {},
  "features": {
    "app_logins": true,
    "browser_login": true,
    "signed_result_links": true,
    "webhooks": true,
    "signup": true,
    "email_code_login": true,
    "web_links": true
  },
  "limits": { "picture": { "max_long_side": 2048, "max_short_side": 1556 } },
  "pricing": {
    "tiers": [
      { "name": "HD", "max_long_side": 1920, "max_short_side": 1080, "eur_per_min": 25 },
      { "name": "2K", "max_long_side": 2048, "max_short_side": 1556, "eur_per_min": 50 }
    ],
    "output_4k_eur_per_min": 50
  },
  "links": {
    "website": "https://www.recuromedia.com/",
    "signup": "https://www.recuromedia.com/",
    "pricing": "https://www.recuromedia.com/",
    "terms": "https://www.recuromedia.com/termsofservice.php",
    "privacy": "https://www.recuromedia.com/privacypolicy.php",
    "support": "https://www.recuromedia.com/menu.aboutus.php#contact-us",
    "developer": "https://www.recuromedia.com/menu.developer.php"
  }
}

Error codes: none of its own. Any endpoint can answer RateLimited and InternalError.

GET /openapi.yaml #

Credential: none - send no Authorization header

Downloads the description of this API in the OpenAPI 3.1 format: every endpoint, its parameters, its responses and every error code. Load it into an API tool (for example Postman or Insomnia) or a code generator to get a ready-made client.

Parameters: none. Send the request exactly as in the example.

What you get back: 200 OK with the YAML text (application/yaml). It can be up to 300 seconds old.

Good to know: The error codes are listed under x-error-codes at the end of the file, each with its HTTP status and whether a retry can help.

Request
curl -sS --fail -o recuro-openapi.yaml "$API/openapi.yaml" || rm -f recuro-openapi.yaml
Response: 200 OK
HTTP/2 200
content-type: application/yaml; charset=utf-8
cache-control: public, max-age=300

Error codes: none of its own. Any endpoint can answer RateLimited and InternalError.

Sign-in

These endpoints are for apps in which a person signs in with an e-mail address and password (our plugins). If you connect your own server with an API key and secret, you need only POST /auth/token (optional) - go on to Account.

POST /auth/token #

Credential: none - send no Authorization header

Gives you a new access token. With an API key (grant_type client_credentials): exchange your key and secret for a token that works for 1 hour, so that your secret travels only once an hour - useful when you send thousands of chunks. In an app (grant_type refresh_token): swap the refresh token for a new pair of tokens before or after the access token expires.

Parameters
NameWhereTypeRequiredWhat it is
Authorizationheaderstringwith client_credentialsHTTP Basic with your API key and secret (curl -u KEY:SECRET). Send no Authorization header with refresh_token.
grant_typebody (JSON)stringyesclient_credentials (API key) or refresh_token (an app).
refresh_tokenbody (JSON)stringwith refresh_tokenThe app's current refresh token (rmr_ and 43 characters).

What you get back: 200 OK with a Tokens object. client_credentials gives no refresh token: when the token expires, exchange the key again. refresh_token gives a new access token and a new refresh token; store the new refresh token at once - the old access token stops working immediately.

Good to know: An old refresh token is accepted once more within 60 seconds (in case the response was lost on the network). Used later than that, it signs the app out (RefreshTokenReused), because someone else must have a copy. At most 30 refreshes per hour per sign-in.

Request
curl -sS -u "$AUTH" "$API/auth/token" -H "Content-Type: application/json" \
     -d '{"grant_type": "client_credentials"}'

curl -sS "$API/auth/token" -H "Content-Type: application/json" \
     -d '{"grant_type": "refresh_token", "refresh_token": "rmr_mQ7cX2pL0dZ8vN4kR6tY1wB9sF3hJ5gA7eU0iO2qW4z"}'
Response: 200 OK (client_credentials)
{
  "token_type": "Bearer",
  "access_token": "rma_nS5lBHPSv6F_cyqYc3RcE293vB2nsxIc0RFxp8A-fMM",
  "expires_in": 3600,
  "scope": "account:read catalogue:read media:read media:write jobs:read jobs:write previews:write keyframes:write webhooks",
  "login_id": "al_OoROQAe6B9wvGUnQM0Uicr"
}

Error codes it can answer (each links to what it means and what to do): ValidationFailed, Unauthorized, InvalidCredentials, RefreshTokenReused, TokenRevoked, AccountDeactivated, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

POST /auth/logout #

Credential: an access token (Bearer)

Signs out the sign-in whose access token you send: its access token and its refresh token stop working at once. This is the Sign out button of an app.

Parameters
NameWhereTypeRequiredWhat it is
AuthorizationheaderstringyesBearer and the access token to sign out.

What you get back: 204 No Content: signed out; the response has no body.

Request
curl -sS -X POST -H "Authorization: Bearer $TOKEN" -d '' "$API/auth/logout"
Response: 204 No Content
HTTP/2 204

Error codes it can answer (each links to what it means and what to do): Unauthorized, TokenExpired, TokenRevoked. Any endpoint can also answer RateLimited and InternalError.

POST /auth/login #

Credential: none - send no Authorization header

Signs a person in from one of our apps with their e-mail address and password, with the same rules and limits as the website's sign-in.

Parameters
NameWhereTypeRequiredWhat it is
emailbody (JSON)stringyesThe account's e-mail address. Example: jane@example.com.
passwordbody (JSON)stringyesThe account's password. Send it once; never store it in the app.
client_idbody (JSON)stringyesWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringno (send it)The app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.

What you get back: A Tokens object (status signed_in), or - when the account uses two-factor authentication - {"status": "2fa_required", "challenge": "...", "expires_in": 300}: send the 6-digit authenticator code with that challenge to POST /auth/login/2fa within 300 seconds.

Good to know: An unknown address, a wrong password and an account made with Google, Apple or Microsoft (it has no password) all get the same InvalidCredentials, so the response never tells whether an address is registered. Such accounts use the browser sign-in or set a password with POST /auth/password/forgot. An account whose e-mail address is not confirmed yet gets AccountNotValidated and we e-mail it a new code (details.signup: send it with the code to POST /auth/signup/verify).

Request
curl -sS "$API/auth/login" -H "Content-Type: application/json" \
     -H "Recuro-Client: premiere-uxp/1.0.0 (Adobe Premiere Pro 26.5; Windows 11)" \
     -d '{"email": "jane@example.com", "password": "her password", "client_id": "premiere-uxp", "client_version": "1.0.0",
          "device_name": "STUDIO", "host_app": "Adobe Premiere Pro", "host_version": "26.5", "os": "Windows 11 24H2"}'
Response: 200 OK
{
  "token_type": "Bearer",
  "access_token": "rma_4QyH0m1Zr8VdP2cX7kLw9sNb3TgE6uJaF5oYiR1tK0e",
  "expires_in": 3600,
  "refresh_token": "rmr_mQ7cX2pL0dZ8vN4kR6tY1wB9sF3hJ5gA7eU0iO2qW4z",
  "refresh_expires_in": 7776000,
  "scope": "account:read catalogue:read media:read media:write jobs:read jobs:write previews:write keyframes:write",
  "login_id": "al_3kT9qLx0aZ7bQm2Rv8YwPc",
  "user": { "id": "usr_1042", "first_name": "Jane", "last_name": "Smith" },
  "company": { "id": "cmp_311", "name": "Example Films Ltd" },
  "status": "signed_in"
}

200 OK (two-factor authentication on)
{ "status": "2fa_required", "challenge": "ch_7f3a9c1e5b2d8f4a6c0e9b1d", "expires_in": 300 }

Error codes it can answer (each links to what it means and what to do): ValidationFailed, InvalidCredentials, AccountNotValidated, AccountDeactivated, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

POST /auth/login/2fa #

Credential: none - send no Authorization header

Finishes a sign-in of an account with two-factor authentication: you send the 6-digit code of the person's authenticator app (for example Google Authenticator) together with the challenge of the sign-in.

Parameters
NameWhereTypeRequiredWhat it is
challengebody (JSON)stringyesThe challenge of the 2fa_required response. It works for 300 seconds.
codebody (JSON)stringyesExactly 6 digits, as a string. Example: "492817".

What you get back: 200 OK with a Tokens object.

Good to know: After 5 wrong codes the challenge ends: sign in again.

Request
curl -sS "$API/auth/login/2fa" -H "Content-Type: application/json" \
     -d '{"challenge": "ch_7f3a9c1e5b2d8f4a6c0e9b1d", "code": "492817"}'
Response: 200 OK
{ "token_type": "Bearer", "access_token": "rma_...", "expires_in": 3600, "refresh_token": "rmr_...", "refresh_expires_in": 7776000,
  "scope": "...", "login_id": "al_3kT9qLx0aZ7bQm2Rv8YwPc", "user": { "...": "..." }, "company": { "...": "..." }, "status": "signed_in" }

Error codes it can answer (each links to what it means and what to do): InvalidCode, RateLimited. Any endpoint can also answer RateLimited and InternalError.

POST /auth/login/code #

Credential: none - send no Authorization header

"E-mail me a code instead": e-mails a 6-digit sign-in code to the address, so that the person can sign in without a password (also accounts made with Google, Apple or Microsoft). Then send the code to POST /auth/login/code/verify.

Parameters
NameWhereTypeRequiredWhat it is
emailbody (JSON)stringyesThe address to send the code to; up to 254 characters.
client_idbody (JSON)stringnoWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringnoThe app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.

What you get back: 202 Accepted with status code_sent_if_registered, a sentence to show (detail) and expires_in (900 seconds: the code works for 15 minutes, once).

Good to know: The response is the same, and takes about the same time, whether or not the address has an account - so it never tells whether an address is registered. The e-mail holds the code only, no link; the app's name (from client_id) is put in it. Limits: 3 codes per address in 15 minutes and 10 a day; 10 and 50 per client address (RateLimited). Available when GET /health says features.email_code_login true.

Request
curl -sS "$API/auth/login/code" -H "Content-Type: application/json" \
     -d '{"email": "jane@example.com", "client_id": "premiere-uxp", "client_version": "1.0.0"}'
Response: 202 Accepted
{ "status": "code_sent_if_registered", "detail": "If jane@example.com has an account, we have e-mailed it a 6-digit code. It works for 15 minutes.", "expires_in": 900 }

Error codes it can answer (each links to what it means and what to do): ValidationFailed, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

POST /auth/login/code/verify #

Credential: none - send no Authorization header

Signs the person in with the e-mailed sign-in code. A right code also confirms the e-mail address.

Parameters
NameWhereTypeRequiredWhat it is
emailbody (JSON)stringyesThe address the code was sent to.
codebody (JSON)stringyesThe 6 digits of the e-mail, as a string.
client_idbody (JSON)stringyesWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringno (send it)The app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.

What you get back: A Tokens object (status signed_in), or - when the account uses two-factor authentication - {"status": "2fa_required", "challenge": "...", "expires_in": 300}: send the 6-digit authenticator code with that challenge to POST /auth/login/2fa within 300 seconds.

Good to know: A wrong code answers InvalidCode with details.tries_left. The fifth wrong code ends the code (details.expired true), and so does its time (15 minutes): ask for a new one. A deactivated account gets AccountDeactivated.

Request
curl -sS "$API/auth/login/code/verify" -H "Content-Type: application/json" \
     -d '{"email": "jane@example.com", "code": "382915", "client_id": "premiere-uxp", "client_version": "1.0.0",
          "device_name": "STUDIO", "host_app": "Adobe Premiere Pro", "host_version": "26.5", "os": "Windows 11 24H2"}'
Response: 200 OK
{ "token_type": "Bearer", "access_token": "rma_...", "expires_in": 3600, "refresh_token": "rmr_...", "refresh_expires_in": 7776000,
  "scope": "...", "login_id": "al_9vR2mQx7aB4cT1nK8wZpLd", "user": { "...": "..." }, "company": { "...": "..." }, "status": "signed_in" }

200 OK (two-factor authentication on)
{ "status": "2fa_required", "challenge": "ch_2b8e5f1a7c3d9e0b4a6f2c8d", "expires_in": 300 }

Error codes it can answer (each links to what it means and what to do): ValidationFailed, InvalidCode, AccountDeactivated, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

POST /auth/signup #

Credential: none - send no Authorization header

Creates a new company account from one of our apps, with the website's rules. We e-mail a 6-digit code; the app sends it to POST /auth/signup/verify to finish. The new account starts on the Trial.

Parameters
NameWhereTypeRequiredWhat it is
first_namebody (JSON)stringyesUp to 30 characters.
last_namebody (JSON)stringyesUp to 30 characters.
companybody (JSON)stringyesThe company's name, up to 100 characters.
emailbody (JSON)stringyesA company address. On the production site, addresses of free and disposable e-mail providers are refused.
passwordbody (JSON)stringyesAt least 10 characters.
countrybody (JSON)stringyesThe two-letter country code (ISO 3166-1 alpha-2). Example: SE. Accounts cannot be opened in the countries under sanctions - KP, CU, IR, SY: they answer SignupInvalid with the field code CountryNotServed.
statebody (JSON)stringin the USAThe two-letter US state code when country is US.
accept_termsbody (JSON)booleanyesMust be true: the person accepts the terms of service.
client_idbody (JSON)stringyesWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringno (send it)The app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.

What you get back: 201 Created with status verify_email, the signup id for the next step and expires_in (3600 seconds: the code works for 60 minutes).

Good to know: An address that has an account already gets the same response - its owner gets an e-mail instead - so the response never tells whether an address is registered. Form errors come as SignupInvalid with one entry per field in details.errors.

Request
curl -sS "$API/auth/signup" -H "Content-Type: application/json" \
     -d '{"first_name": "Jane", "last_name": "Smith", "company": "Example Films Ltd", "email": "jane@example-films.com",
          "password": "a long pass phrase", "country": "SE", "accept_terms": true,
          "client_id": "premiere-uxp", "client_version": "1.0.0", "device_name": "STUDIO", "host_app": "Adobe Premiere Pro", "host_version": "26.5", "os": "macOS 15.6"}'
Response: 201 Created
{ "status": "verify_email", "signup": "su_4b8e1f0a9c3d7e2b5a6f1c08", "expires_in": 3600 }

Error codes it can answer (each links to what it means and what to do): SignupInvalid, SignupClosed, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

POST /auth/signup/verify #

Credential: none - send no Authorization header

Finishes a sign-up (or the sign-in of an account whose address was not confirmed yet) with the e-mailed code: the account is confirmed and the app is signed in.

Parameters
NameWhereTypeRequiredWhat it is
signupbody (JSON)stringyesThe signup id of POST /auth/signup (or details.signup of AccountNotValidated).
codebody (JSON)stringyesThe 6 digits of the e-mail, as a string.
client_idbody (JSON)stringyesWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringno (send it)The app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.

What you get back: A Tokens object (status signed_in), or - when the account uses two-factor authentication - {"status": "2fa_required", "challenge": "...", "expires_in": 300}: send the 6-digit authenticator code with that challenge to POST /auth/login/2fa within 300 seconds.

Request
curl -sS "$API/auth/signup/verify" -H "Content-Type: application/json" \
     -d '{"signup": "su_4b8e1f0a9c3d7e2b5a6f1c08", "code": "305118", "client_id": "premiere-uxp", "client_version": "1.0.0", "device_name": "STUDIO"}'
Response: 200 OK
{ "token_type": "Bearer", "access_token": "rma_...", "refresh_token": "rmr_...", "...": "...", "status": "signed_in" }

Error codes it can answer (each links to what it means and what to do): InvalidCode, RateLimited. Any endpoint can also answer RateLimited and InternalError.

POST /auth/signup/resend #

Credential: none - send no Authorization header

Sends a new code for a sign-up whose e-mail did not arrive.

Parameters
NameWhereTypeRequiredWhat it is
signupbody (JSON)stringyesThe signup id.

What you get back: 202 Accepted with the same fields as POST /auth/signup. The response comes after about 2.5 seconds, whether or not anything was sent.

Good to know: At most 3 new codes per sign-up, at least one minute apart. New codes have their own limits (3 per address in 15 minutes and 6 a day; 10 and 30 per client address), separate from the sign-up's first e-mail, so all 3 can be sent.

Request
curl -sS "$API/auth/signup/resend" -H "Content-Type: application/json" -d '{"signup": "su_4b8e1f0a9c3d7e2b5a6f1c08"}'
Response: 202 Accepted
{ "status": "verify_email", "signup": "su_4b8e1f0a9c3d7e2b5a6f1c08", "expires_in": 3600 }

Error codes it can answer (each links to what it means and what to do): RateLimited. Any endpoint can also answer RateLimited and InternalError.

POST /auth/password/forgot #

Credential: none - send no Authorization header

E-mails a 6-digit code for a new password, when the address has an account. Then send the code with the new password to POST /auth/password/reset.

Parameters
NameWhereTypeRequiredWhat it is
emailbody (JSON)stringyesThe account's e-mail address.

What you get back: 202 Accepted with status code_sent_if_registered and a sentence to show. The response is the same whether or not the address has an account. The code works for 15 minutes.

Request
curl -sS "$API/auth/password/forgot" -H "Content-Type: application/json" -d '{"email": "jane@example.com"}'
Response: 202 Accepted
{ "status": "code_sent_if_registered", "detail": "If jane@example.com has an account, we have e-mailed it a 6-digit code. It works for 15 minutes." }

Error codes it can answer (each links to what it means and what to do): ValidationFailed, RateLimited. Any endpoint can also answer RateLimited and InternalError.

POST /auth/password/reset #

Credential: none - send no Authorization header

Sets a new password with the e-mailed code. Every other app of the account is signed out (as a reset on the website does) and this app is signed in.

Parameters
NameWhereTypeRequiredWhat it is
emailbody (JSON)stringyesThe account's e-mail address.
codebody (JSON)stringyesThe 6 digits of the e-mail, as a string.
new_passwordbody (JSON)stringyesAt least 10 characters.
client_idbody (JSON)stringyesWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringno (send it)The app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.

What you get back: A Tokens object (status signed_in), or - when the account uses two-factor authentication - {"status": "2fa_required", "challenge": "...", "expires_in": 300}: send the 6-digit authenticator code with that challenge to POST /auth/login/2fa within 300 seconds. A reset does not skip the second factor.

Request
curl -sS "$API/auth/password/reset" -H "Content-Type: application/json" \
     -d '{"email": "jane@example.com", "code": "771204", "new_password": "a new long pass phrase", "client_id": "premiere-uxp", "client_version": "1.0.0"}'
Response: 200 OK
{ "token_type": "Bearer", "access_token": "rma_...", "refresh_token": "rmr_...", "...": "...", "status": "signed_in" }

Error codes it can answer (each links to what it means and what to do): WeakPassword, InvalidCode, AccountDeactivated, RateLimited. Any endpoint can also answer RateLimited and InternalError.

POST /auth/app-logins #

Credential: none - send no Authorization header

Starts a browser sign-in, for accounts made with Google, Apple or Microsoft (they have no password). The person approves the app on our website in their normal browser; the app waits and then collects the tokens.

Parameters
NameWhereTypeRequiredWhat it is
client_idbody (JSON)stringyesWhich of our apps signs in: premiere-uxp, resolve, avid-helper, ofx. Other values are refused (ValidationFailed).
client_versionbody (JSON)stringno (send it)The app's version, up to 20 characters. Shown under Connected Apps. Example: 1.0.0.
device_namebody (JSON)stringnoThe computer's name, up to 100 characters. Shown under Connected Apps. Example: STUDIO.
host_appbody (JSON)stringnoThe editing application, up to 60 characters. Example: Adobe Premiere Pro.
host_versionbody (JSON)stringnoIts version, up to 30 characters. Example: 26.5.
osbody (JSON)stringnoThe operating system, up to 60 characters. Example: Windows 11 24H2.
code_challengebody (JSON)stringyesThe PKCE challenge: the SHA-256 of your verifier, base64url without padding - exactly 43 characters (see the example under Browser sign-in).
code_challenge_methodbody (JSON)stringyesOnly S256.

What you get back: 201 Created with: login_id (for the next two endpoints); user_code (show it in the app); verification_uri and verification_uri_complete (open the complete one in the system browser); expires_in (900 seconds: the request ends after 15 minutes); interval (seconds to wait between two polls of the token, 5).

Good to know: At most 10 browser sign-ins started per client address in 10 minutes.

Request
curl -sS "$API/auth/app-logins" -H "Content-Type: application/json" \
     -d '{"client_id": "premiere-uxp", "client_version": "1.0.0", "device_name": "STUDIO", "host_app": "Adobe Premiere Pro", "host_version": "26.5",
          "os": "Windows 11 24H2", "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "code_challenge_method": "S256"}'
Response: 201 Created
{
  "login_id": "al_B2HbSt2AsdOBB7nK9njIhY",
  "user_code": "LRDL-XQTG",
  "verification_uri": "https://www.recuromedia.com/connect.php",
  "verification_uri_complete": "https://www.recuromedia.com/connect.php?login=al_B2HbSt2AsdOBB7nK9njIhY",
  "expires_in": 900,
  "interval": 5
}

Error codes it can answer (each links to what it means and what to do): ValidationFailed, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

POST /auth/app-logins/{login_id}/token #

Credential: none - the PKCE verifier in the body proves who you are

Asks whether the person has approved the browser sign-in yet, and collects the tokens once they have. Poll it every interval seconds.

Parameters
NameWhereTypeRequiredWhat it is
login_idpathstringyesThe login_id of POST /auth/app-logins.
code_verifierbody (JSON)stringyesThe PKCE verifier the app made (it never leaves the app before this request).

What you get back: 202 Accepted with {"status": "pending", "interval": 5, "expires_in": ...} while the person has not decided; 200 OK with a Tokens object after Allow - exactly once. Deny: AppLoginDenied.

Good to know: Polling faster than interval answers SlowDown with a longer interval in details.interval: use it from then on. At most 60 polls per minute per client address.

Request
curl -sS "$API/auth/app-logins/al_B2HbSt2AsdOBB7nK9njIhY/token" -H "Content-Type: application/json" \
     -d '{"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"}'
Response: 202 Accepted (not decided yet), then 200 OK with the tokens
{ "status": "pending", "interval": 5, "expires_in": 880 }

Error codes it can answer (each links to what it means and what to do): InvalidCredentials, SlowDown, AppLoginDenied, AppLoginExpired, AppLoginAlreadyRedeemed. Any endpoint can also answer RateLimited and InternalError.

DELETE /auth/app-logins/{login_id} #

Credential: none - the PKCE verifier in the body proves who you are

Cancels a browser sign-in that is still waiting - the app's Cancel button.

Parameters
NameWhereTypeRequiredWhat it is
login_idpathstringyesThe login_id of POST /auth/app-logins.
code_verifierbody (JSON)stringyesThe same PKCE verifier.

What you get back: 204 No Content: cancelled; no body.

Request
curl -sS -X DELETE "$API/auth/app-logins/al_B2HbSt2AsdOBB7nK9njIhY" -H "Content-Type: application/json" \
     -d '{"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"}'
Response: 204 No Content
HTTP/2 204

Error codes it can answer (each links to what it means and what to do): AppLoginExpired. Any endpoint can also answer RateLimited and InternalError.

POST /auth/web-links #

Credential: an access token (Bearer) · scope account:read

Opens a page of our website signed in, from an app the person is signed in to - the app's Top up..., Order history..., Account..., Open the Website, Connected Apps and Contact Support buttons, without a second sign-in. The answer is a one-time link: open it in the system browser at once. See Website pages from an app.

Parameters
NameWhereTypeRequiredWhat it is
AuthorizationheaderstringyesBearer and the access token of an app sign-in.
pagebody (JSON)stringyesWhich page: top_up (Top Up on a Prepaid plan; the upgrade dialog on the Trial; a short note on the other plans), orders (Order History), workspace, account (Account Settings), connected_apps (Connected Apps) or support (the Contact Us dialog).

What you get back: 201 Created with url (open it in the system browser), page, expires_in (60 seconds) and expires_at.

Good to know: Only for app sign-ins: an API key, or a token made from one, gets InsufficientScope (details.required app_login). A link works once, for 60 seconds, and only while this sign-in is live. At most 5 links a minute per sign-in. Whether to offer it: features.web_links of GET /health and account.web_links of GET /me.

Request
curl -sS "$API/auth/web-links" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
     -d '{"page": "top_up"}'
Response: 201 Created
{
  "url": "https://www.recuromedia.com/auth.weblink.php?page=top_up#t=Vq3kQ0d6b5bJ0fW1nXlq8m5yC3o4x2RkTt7uA9sZpEg",
  "page": "top_up",
  "expires_in": 60,
  "expires_at": "2026-10-15T14:03:38Z"
}

Error codes it can answer (each links to what it means and what to do): ValidationFailed, InsufficientScope, TokenRevoked, RateLimited, FeatureUnavailable. Any endpoint can also answer RateLimited and InternalError.

Account

Who is signed in, what you can order and what it costs.

GET /me #

Credential: API key and secret (HTTP Basic) or an access token (Bearer) · scope account:read

Tells you who you are signed in as and what your account can do: the user, the company and its plan, the balance or the seconds left, your prices, your limits and links to the website pages for what the API does not do (paying, account settings). Use it as your first request: if it works, your key and secret are right.

Parameters
NameWhereTypeRequiredWhat it is
If-None-MatchheaderstringnoThe ETag of your last GET /me: when nothing has changed you get 304 Not Modified without a body - free: a 304 never counts against your limits. See Polling and ETags.

What you get back: 200 OK with a Me object and an ETag header.

Request
curl -sS -u "$AUTH" "$API/me"
Response: 200 OK
{
  "user": { "id": "usr_1042", "first_name": "Jane", "last_name": "Smith", "email": "jane@example.com", "two_factor": false,
            "api_enabled": true },
  "company": { "id": "cmp_311", "name": "Example Films Ltd", "account_type": "client" },
  "plan": { "code": "prepaid", "name": "Prepaid", "watermark": false, "billing": "balance" },
  "balance": { "amount": "412.50", "currency": "EUR" },
  "quota": null,
  "limits": {
    "trial_max_seconds": null,
    "max_upload_bytes": 53687091200,
    "max_open_uploads": 10,
    "chunk_size": { "min": 4194304, "max": 33554432, "default": 8388608 },
    "disk_used_percent": 12.5,
    "previews_left_today": { "restoration": 20, "colorization": 5 },
    "active_previews_max": 2
  },
  "prices": {
    "currency": "EUR",
    "discount_percent": 0.0,
    "per_minute": {
      "restoration": { "sd": "25.00", "hd": "25.00", "2k": "50.00", "4k": "50.00" },
      "colorization": { "sd": "25.00", "hd": "25.00", "2k": "50.00", "4k": "50.00" }
    },
    "minimum": null,
    "rule": "Every job costs exactly its seconds, from zero, at the per-minute price of its tier (the larger of the input picture's tier - HD or 2K - and a 4K output), the same for every filter, rounded to the nearest 0.10. Pictures larger than 2K are not taken."
  },
  "capabilities": { "colorize": true, "reconstruct": true, "interpolate": true, "audio": true, "match_source": true,
                    "keyframes_auto": true, "ranges": false, "previews": true, "webhooks": true },
  "defaults": { "handles_seconds": 1, "output_format": "mov_prores_422_hq", "keyframes": "review" },
  "links": {
    "workspace": "https://www.recuromedia.com/workspace.php",
    "top_up": "https://www.recuromedia.com/workspace.php",
    "account": "https://www.recuromedia.com/dialog.accountsettings.php",
    "connected_apps": "https://www.recuromedia.com/dialog.connectedapps.php",
    "order_history": "https://www.recuromedia.com/dialog.orderhistory.php",
    "support": "https://www.recuromedia.com/menu.aboutus.php#contact-us",
    "terms": "https://www.recuromedia.com/termsofservice.php",
    "privacy": "https://www.recuromedia.com/privacypolicy.php",
    "upgrade": "https://www.recuromedia.com/workspace.php",
    "pricing": "https://www.recuromedia.com/workspace.php"
  },
  "auth": { "kind": "api_key", "client_id": "api-key", "login_id": null,
            "scopes": ["account:read", "catalogue:read", "media:read", "media:write", "jobs:read", "jobs:write", "previews:write", "keyframes:write", "webhooks"],
            "access_expires_at": null, "device_name": null },
  "account": { "billing": "prepaid", "can_top_up": true, "can_upgrade": false, "billing_note": null, "web_links": false, "web_pages": [],
               "pages": { "top_up": "https://www.recuromedia.com/workspace.php?open=top_up", "orders": "https://www.recuromedia.com/workspace.php?open=orders",
                          "workspace": "https://www.recuromedia.com/workspace.php", "account": "https://www.recuromedia.com/workspace.php?open=account",
                          "connected_apps": "https://www.recuromedia.com/workspace.php?open=connected_apps", "support": "https://www.recuromedia.com/workspace.php?open=support" } }
}

Error codes it can answer (each links to what it means and what to do): Unauthorized, InvalidCredentials, ApiAccessDisabled. Any endpoint can also answer RateLimited and InternalError.

GET /catalogue #

Credential: API key and secret (HTTP Basic) or an access token (Bearer) · scope catalogue:read

Lists everything a job can be made of: the filters, the filters of which you can choose only one, the output formats and sizes, the output options and the presets (ready-made filter sets - ours and the ones you saved in the workspace). Read it once when your program starts and use its keys in settings.

Parameters
NameWhereTypeRequiredWhat it is
If-None-MatchheaderstringnoThe ETag of your last GET /catalogue (a free 304 when nothing has changed).

What you get back: 200 OK with a Catalogue object and an ETag header. The example above is shortened: the real response lists all 23 filters, 6 output formats and every preset you can use.

Request
curl -sS -u "$AUTH" "$API/catalogue"
Response: 200 OK
{
  "presets": [
    { "id": "pre_90", "name": "Restore without Grain", "owner": "global",
      "filters": ["noise", "medium_dust_and_scratches", "color_balance", "color_saturation", "contrast_expansion", "image_stabilization",
                  "adaptive_sharpness", "vertical_scratches", "normalize_volume", "reduce_noise", "increase_clarity"],
      "output_format": "mov_prores_422_hq", "output_scaling": "source", "colorize": false, "allowed": true }
  ],
  "filters": [
    { "key": "noise", "group": "dust_and_noise", "label": "Noise", "stage": "restoration", "kind": "video", "changes_timing": false,
      "price_class": "restoration", "allowed": true,
      "description": "Removes all inconsistent noise between frames and creates a smoother frame appearance.",
      "example_url": "https://www.recuromedia.com/movies/Dust%20and%20Noise.mp4" }
  ],
  "exclusive_groups": [["light_dust_and_scratches", "medium_dust_and_scratches", "heavy_dust_and_scratches"]],
  "output_formats": [
    { "key": "mov_prores_422_hq", "label": "MOV (ProRes 422 HQ)", "container": "mov", "edit_friendly": true }
  ],
  "output_scalings": [
    { "key": "source", "label": "Same as source" }, { "key": "1080", "label": "HD (1080)" }, { "key": "2160", "label": "4K (2160)" }
  ],
  "output_options": { "geometry": ["restored", "match_source"], "frame_rate": ["processed", "source"], "audio": ["source", "restored", "none"] },
  "limits": { "picture": { "max_long_side": 2048, "max_short_side": 1556 } },
  "pricing": { "tiers": [ { "name": "HD", "max_long_side": 1920, "max_short_side": 1080, "eur_per_min": 25 },
                          { "name": "2K", "max_long_side": 2048, "max_short_side": 1556, "eur_per_min": 50 } ],
               "output_4k_eur_per_min": 50 }
}

Error codes: none of its own. Any endpoint can answer RateLimited and InternalError.

POST /quotes #

Credential: API key and secret (HTTP Basic) or an access token (Bearer) · scope catalogue:read

Tells you what a job would cost and whether it would start, without starting it. It runs exactly the checks the start runs. Ask it for a media you have uploaded (media_id), or for a file you have not uploaded yet (planned: its length and frame size).

Parameters
NameWhereTypeRequiredWhat it is
media_idbody (JSON)stringthis or plannedA ready media of yours. Example: med_4821.
plannedbody (JSON)objectthis or media_idA file you have not uploaded yet.
planned.duration_secondsbody (JSON)numberwith plannedIts length in seconds: more than 0, at most 86400 (24 hours). Example: 42.0.
planned.widthbody (JSON)integernoIts frame width in pixels, 0 to 16384. 0 (and height 0) for a sound-only file. Default: 0. For a video give both: left out, the file is quoted as sound only (tier sd) - a 2K film would be quoted at the HD price.
planned.heightbody (JSON)integernoIts frame height in pixels, 0 to 16384. Give both or neither. Default: 0. A picture larger than 2048 × 1556 (either way round) answers PictureTooLarge: it could not be uploaded.
planned.frame_ratebody (JSON)objectnoIts frame rate as the file has it: whole numbers num and den, for example {"num": 24000, "den": 1001} for 23.976 fps or {"num": 25, "den": 1}; at most 1000 fps. Given, an MXF output at a frame rate MXF cannot hold (for example 16 or 18 fps) is refused at the quote. Left out, that check waits for the start.
settingsbody (JSON)objectyesWhat the job does: either preset_id or filters (not both), optionally output_format and output_scaling.
settings.preset_idbody (JSON)stringone of the twoA preset of GET /catalogue (presets[].id): its filters, output format and output scaling. Example: pre_90. A preset's upload server (a website setting) is never used by an API job: the result stays here for you to download.
settings.filtersbody (JSON)array of stringsone of the twoFilter keys of GET /catalogue (filters[].key; the full list is in Filters). At most one of light_dust_and_scratches, medium_dust_and_scratches and heavy_dust_and_scratches. An empty list [] runs no filter: the job only re-encodes the media into the output format (and scales it, with output_scaling), at the full price of its tier. Example: ["noise", "medium_dust_and_scratches"].
settings.output_formatbody (JSON)stringnoThe result's file type: mov_prores_422_hq, mov_prores_4444, mxf_dnxhr_hqx, mxf_dnxhr_444, mp4_h264 or mp4_hevc. Default: the preset's format, or mov_prores_422_hq with filters.
settings.output_scalingbody (JSON)stringnoThe result's height: source (the same as the media), 1080 (HD) or 2160 (4K). Default: the preset's scaling, or source with filters. A larger output can raise the price (see Billing).
outputbody (JSON)objectnoAs for POST /jobs (a value it refuses answers ValidationFailed here too); only output.format and output.scaling change the price. Send the same options you will start the job with: combinations the start refuses are refused here.
rangebody (JSON)nullnoLeave it out. Any other value than null answers RangeNotSupported.

What you get back: 200 OK with a Quote object. covers false means the job would be refused now; refusal then holds the error code, sentence and details the start would give (for example BalanceInsufficient). A quote is not stored and reserves nothing.

Good to know: The quote runs the start's own checks, in the start's order: first the media, the output container and the output options - the combinations POST /jobs refuses with OptionConflict (an MXF output at a frame rate MXF cannot hold or smaller than 256×120 pixels, MXF or output.audio none for a sound-only file, a sound-only media whose sound is silent, match_source with an output scaling or with a stretched or squeezed picture, output.frame_rate source with natural speed or high frame rate) and FrameRateChangedAtIngest - then the balance or quota and the trial's clip length. Each answers covers false with that refusal, so your program can show it before anything is exported or uploaded. A planned file has not been analysed yet: its checks use what you send (the frame size, sound only, planned.frame_rate); the crop, silent sound, a stretched picture and a removed pulldown are known only after the analysis, so those checks wait for the start.

Request
curl -sS -u "$AUTH" "$API/quotes" -H "Content-Type: application/json" \
     -d '{"planned": {"duration_seconds": 42.0, "width": 1920, "height": 1080},
          "settings": {"filters": ["noise", "medium_dust_and_scratches"], "output_format": "mov_prores_422_hq"}}'
Response: 200 OK
{
  "price": { "amount": "17.50", "currency": "EUR" },
  "charge": { "amount": "17.50", "currency": "EUR", "from": "balance" },
  "tier": "hd",
  "price_class": "restoration",
  "duration_seconds": 42.0,
  "covers": true,
  "refusal": null,
  "watermark": false,
  "estimate": true,
  "exchange": null,
  "settings": { "preset_id": null, "filters": ["noise", "medium_dust_and_scratches"], "output_format": "mov_prores_422_hq", "output_scaling": "source" }
}

Error codes it can answer (each links to what it means and what to do): ValidationFailed, NotFound, MediaNotReady, PictureTooLarge, PictureTooLargeForFilter, RangeNotSupported, OptionConflict. Any endpoint can also answer RateLimited and InternalError.