Skip to content
2a.qa

API reference

2a.qa Public API · v1.0.0 · OpenAPI 3.1.0

The complete 2a.qa API reference. Everything on this page is generated from the same document served at openapi.json — it cannot drift from it.

Base URL

https://2a.qa/api/v1

Machine-readable document (OpenAPI 3.1)

/api/v1/openapi.json

Authentication

Every request carries a key in an Authorization header. There is no session, no cookie and no CSRF token on /api/v1 — the route you open in a signed-in browser is not the route your bot talks to.

Authorization: Bearer 2aq_live_…

Scopes

links:readlinks:writeanalytics:readassets:readassets:writelogs:readlogs:write
Issue a key from API key settings

Quickstart

curl -X POST https://2a.qa/api/v1/links \
  -H "Authorization: Bearer 2aq_live_***" \
  -H "Content-Type: application/json" \
  -d '{"originalUrl":"https://example.com/campaign",
       "customAlias":"sale"}'

# HTTP/2 201 Created
{"link":{"id":"clx…","shortCode":"sale","customAlias":"sale",
         "originalUrl":"https://example.com/campaign",
         "hasPassword":false,"trustPreview":false}}

The response does not carry a ready-made short URL — build it from the base URL and shortCode. That is deliberate: a custom domain can change it.

Files

List, inspect, rename and delete. The upload itself is on /api/files, not v1.

GET/filesfiles:read

List shared files (never the password hash — only hasPassword)

Responses

200
OK
403
Missing scope: files:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
GET/files/{id}files:read

One shared file

Responses

200
OK
403
Missing scope: files:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
PATCH/files/{id}files:write

Rename or re-schedule a file. password/slug/mimeType are refused by name

Responses

200
OK
403
Missing scope: files:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
DELETE/files/{id}files:write

Delete a file — archived to the black box FIRST, then removed

Responses

200
OK
403
Missing scope: files:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment

Bio page

The page and its links — http/https only, Safe Browsing checked.

GET/biobio:read

The bio page and its links

Responses

200
OK
403
Missing scope: bio:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
PATCH/biobio:write

Edit displayName, bio, isPublished, filesEnabled. Appearance and slug are dashboard-only and refused by name

Responses

200
OK
403
Missing scope: bio:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
POST/bio/linksbio:write

Add a bio link (http/https only, Safe Browsing checked, max 30)

Responses

200
OK
403
Missing scope: bio:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment

Webhooks

Subscriptions and delivery log. The signing secret appears in the create response only.

GET/webhookswebhooks:read

List webhooks. The HMAC secret is NEVER in a list response

Responses

200
OK
403
Missing scope: webhooks:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
POST/webhookswebhooks:write

Create a webhook — the 201 body is the ONLY place the secret is ever shown

Responses

200
OK
403
Missing scope: webhooks:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
PATCH/webhooks/{id}webhooks:write

Toggle, re-subscribe or re-describe. url is not editable — delete and recreate

Responses

200
OK
403
Missing scope: webhooks:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
DELETE/webhooks/{id}webhooks:write

Delete a webhook

Responses

200
OK
403
Missing scope: webhooks:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
GET/webhooks/{id}/deliverieswebhooks:read

Delivery history for one webhook, newest first (?limit=1..100)

Responses

200
OK
403
Missing scope: webhooks:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment

Domains

Custom domains and their verification.

GET/domainsdomains:read

List custom domains and the CNAME target

Responses

200
OK
403
Missing scope: domains:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
POST/domainsdomains:write

Register a hostname and get its CNAME + TXT records back

Responses

200
OK
403
Missing scope: domains:write, or the account is not entitled to this section
404
Not this account's, or not there
409
Hostname already registered — deliberately without saying by whom
503
Switched off on this deployment
POST/domains/{id}/verifydomains:write

Re-run DNS verification. 200 with ok:false means the check RAN and the records are not right yet

Responses

200
OK
403
Missing scope: domains:write, or the account is not entitled to this section
404
Not this account's, or not there
429
30 verifications per hour per account
503
Switched off on this deployment
DELETE/domains/{id}/verifydomains:write

Remove a custom domain

Responses

200
OK
403
Missing scope: domains:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment

Profile

Name and handle only — email, password and role are refused by name.

GET/profileprofile:read

The account profile

Responses

200
OK
403
Missing scope: profile:read, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment
PATCH/profileprofile:write

Edit name and handle ONLY. email, password, role and isActive are refused by name — an API key must never be an account takeover

Responses

200
OK
403
Missing scope: profile:write, or the account is not entitled to this section
404
Not this account's, or not there
503
Switched off on this deployment

Assets (Majra)

Upload, list and delete bytes, plus upload tickets for untrusted clients.

GET/assetsassets:read

List assets

Parameters

NameInTypeRequired
qquerystring
kindqueryIMAGE | VIDEO | AUDIO
pathquerystring
sourcequerystring
fromquerystring · date-time
toquerystring · date-time
pagequeryinteger
pageSizequeryinteger · ≤100

Responses

200
Page of assets, newest first. List items carry MORE fields than GET /assets/{id} does — originalUrl, width, height, durationMs and source are here and not there
400
`from` or `to` is not a parseable RFC 3339 timestamp
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:read, or this account is not enrolled in Majra
503
Majra is not enabled on this deployment

Response shape (200)

itemsAssetListItem[]
totalinteger
pageinteger
pageSizeinteger
POST/assetsassets:write

Upload an asset

Responses

200
Deduplicated — identical bytes were already stored for this account, and the existing asset is returned
201
Created
400
Not multipart/form-data, missing `file`, empty upload, invalid `metadata`, or `retentionExempt` sent together with `expiresInDays`
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:write, or this account is not enrolled in Majra
409
Upload could not be committed under write contention — retry
413
Asset exceeds the per-asset size limit, or the account storage quota is reached
415
Unsupported media type. Allowed: png, jpeg, gif, webp, avif, mp4, webm, mp3, ogg, wav
429
Too many uploads (300/hour per key and IP), or the platform-wide storage ceiling is reached
500
Bytes could not be stored
503
Majra is not enabled on this deployment

Response shape (200)

assetIngestedAsset
dedupedboolean
POST/assets/base64assets:write

Upload an asset as base64

Request body

NameTypeRequired
base64stringyes
filenamestring · ≤200
pathstring · ≤163
metadataobject
retentionExemptboolean
expiresInDaysinteger
sourcestring · ≤60

Responses

200
Deduplicated — identical bytes were already stored for this account, and the existing asset is returned
201
Created
400
Body is not a JSON object, `base64` missing or not a string, not valid base64, decodes to nothing, invalid `metadata`, or `retentionExempt` sent together with `expiresInDays`
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:write, or this account is not enrolled in Majra
409
Upload could not be committed under write contention — retry
413
Decoded asset exceeds the per-asset size limit, or the account storage quota is reached
415
Unsupported media type. Allowed: png, jpeg, gif, webp, avif, mp4, webm, mp3, ogg, wav
429
Too many uploads (300/hour per key and IP), or the platform-wide storage ceiling is reached
500
Bytes could not be stored
503
Majra is not enabled on this deployment

Response shape (200)

assetIngestedAsset
dedupedboolean
POST/assets/ticketsassets:write

Mint a single-use upload ticket

Pre-commits every constraint here, by the authenticated caller. The redeeming client cannot widen them and cannot set them at all — its `path`, `metadata` and `retentionExempt` are IGNORED, not merged. `token` is returned once; only its sha256 is stored.

Request body

NameTypeRequired
maxBytesinteger
allowedKindsIMAGE | VIDEO | AUDIO[]
pathstring · ≤163
metadataobject
retentionExemptboolean
ttlSecinteger · ≤3600

Responses

201
Minted
400
Unknown `allowedKinds` value, or invalid `metadata`
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:write, or this account is not enrolled in Majra
429
Too many tickets (600/hour per key and IP)
503
Majra is not enabled on this deployment

Response shape (201)

POST/assets/tickets/{token}no key

Redeem an upload ticket (no API key)

multipart/form-data, field `file`. Any `path`, `metadata` or `retentionExempt` in the body is IGNORED — the ticket's pre-committed values win. The ticket is claimed BEFORE the bytes are read and is never returned to the pool: a failed upload needs a NEW ticket.

Parameters

NameInTypeRequired
tokenpathstringyes

Responses

200
Deduplicated — identical bytes were already stored for the minting account
201
Created
400
Not multipart/form-data, or missing `file`
403
The MINTING account is no longer enrolled in Majra
409
Upload could not be committed under write contention — mint a new ticket and retry
410
Malformed, already used, or expired token — one answer for all three, so a grinder cannot learn which guesses had the right shape
413
Asset exceeds the ticket's `maxBytes` or the platform ceiling, or the minting account's storage quota is reached
415
Unsupported media type, or a kind outside the ticket's `allowedKinds`
429
Too many redemptions (30/minute per IP; fails CLOSED when the limiter is unreachable)
500
Bytes could not be stored
503
Majra is not enabled on this deployment

Response shape (200)

assetIngestedAsset
dedupedboolean
GET/assets/{id}assets:read

Get one asset

Parameters

NameInTypeRequired
idpathstringyes

Responses

200
The asset. Note it does NOT carry originalUrl, width, height, durationMs or source — the list route does
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:read, or this account is not enrolled in Majra
404
No such asset, or it belongs to another account — the two are deliberately indistinguishable
503
Majra is not enabled on this deployment

Response shape (200)

PATCH/assets/{id}assets:write

Update an asset's display fields

Parameters

NameInTypeRequired
idpathstringyes

Request body

NameTypeRequired
filenamestring · ≤200
pathstring · ≤163
retentionExemptboolean
expiresInDaysinteger · nullable

Responses

200
Updated. Only the editable fields are echoed back
400
Body is not a JSON object, no editable field was sent (filename, path, retentionExempt, expiresInDays), or the result would be both `retentionExempt` and expiring
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:write, or this account is not enrolled in Majra
404
No such asset, or it belongs to another account
503
Majra is not enabled on this deployment

Response shape (200)

DELETE/assets/{id}assets:write

Delete an asset

Parameters

NameInTypeRequired
idpathstringyes

Responses

200
Deleted. `bytesReclaimed` is false when another asset still references the same content hash
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:write, or this account is not enrolled in Majra
404
No such asset, or it belongs to another account
503
Majra is not enabled on this deployment

Response shape (200)

deletedboolean
idstring
bytesReclaimedboolean

Events & alerts (Majra)

Ingest and query events, alert rules, and datasets.

GET/logslogs:read

Query log events

Parameters

NameInTypeRequired
datasetquerystring
X-2aqa-Datasetheaderstring
qquerystring
levelquerystring
resourcequerystring
traceIdquerystring
metadata.<path>querystring
fromquerystring · date-time
toquerystring · date-time
beforequerystring
orderqueryasc | desc
includeTotalquerytrue | false | 1 | 0
limitqueryinteger · ≤100

Responses

200
Page of events, newest first. `total` is null unless `includeTotal=true`
400
No dataset named (header or query), unknown `level`, `order` other than asc/desc, unparseable `from`/`to`, `from` after `to`, a window wider than 31 days, more than 5 `metadata.` filters, an empty `metadata.` path, a malformed `before` cursor, or a `source` param (not a column on events — use `resource`)
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:read, or this account is not enrolled in Majra
404
Unknown dataset, or it belongs to another account. A PAUSED dataset still reads normally
429
Query rate limit exceeded (120/minute)
503
Majra is not enabled on this deployment

Response shape (200)

itemsLogEvent[]
nextBeforestring · nullable
hasMoreboolean
totalinteger · nullable
POST/logslogs:write

Ingest log events

Parameters

NameInTypeRequired
X-2aqa-Datasetheaderstring
datasetquerystring

Responses

202
Batch accepted — partially, if some elements were rejected. A batch where EVERY element was rejected is still a 202, with `accepted: 0` and no dataset created: check `accepted`, never the status code. `datasetCreated` is true when the name auto-created a new stream, which is how a typo'd dataset name surfaces
400
No dataset named (header or query), a dataset name that breaks `^[a-z0-9][a-z0-9_-]{0,63}$`, an unreadable body, invalid JSON, a body that is not an ARRAY (a bare object is refused — wrap it as `[{ … }]`), or an EMPTY array. An all-rejected batch is NOT a 400 — it is a 202 with `accepted: 0`
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, this account is not enrolled in Majra, or the dataset is paused
413
Batch exceeds 500 events or a 1 MB body
429
Ingest rate limit exceeded (600/minute, metered per API key — not per IP)
500
The dataset could not be opened, or the events could not be stored. Nothing was written
503
Log ingest is not enabled on this deployment

Response shape (202)

statusstring
acceptedinteger
rejectedinteger
datasetCreatedboolean
errorsobject[]
GET/logs/{id}logs:read

Get one log event

Parameters

NameInTypeRequired
idpathstringyes

Responses

200
The event. It carries THREE fields the list route omits — `dataset`, `apiKeyId` and the truncated `ip`
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:read, or this account is not enrolled in Majra
404
No such event, or it belongs to another account
503
Majra is not enabled on this deployment

Response shape (200)

GET/alertslogs:read

List log-alert rules

Parameters

NameInTypeRequired
pagequeryinteger
pageSizequeryinteger · ≤100

Responses

200
Paged rules
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:read, or this account is not enrolled in Majra
503
Majra is not enabled on this deployment

Response shape (200)

itemsAlertRule[]
totalinteger
pageinteger
pageSizeinteger
POST/alertslogs:write

Create a log-alert rule

Fires the `log_alert` webhook when `threshold` matching events arrive within `windowSec`. Debounced: a rule that fires goes quiet for its whole window, so a crash loop emitting ten thousand errors sends one notification. At least one of `minLevel`, `messageContains` or `metadataMatch` is required — a rule with no predicate matches every event.

Request body

NameTypeRequired
namestring · ≤120yes
datasetstring · nullable
minLevelfatal | error | warn | info | debug |
messageContainsstring · nullable · ≤200
metadataMatchobject · nullable
thresholdinteger · ≤100000
windowSecinteger · ≤86400
isActiveboolean

Responses

201
Created
400
Not a JSON object, unknown dataset, a value out of range, or no predicate at all
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, or this account is not enrolled in Majra
409
At most 50 alert rules per account
503
Majra is not enabled on this deployment

Response shape (201)

PATCH/alerts/{id}logs:write

Update a log-alert rule

Editing any predicate (`minLevel`, `messageContains`, `metadataMatch`) also CLEARS the debounce, so a rule changed to catch something urgent is not silent for the rest of its window.

Parameters

NameInTypeRequired
idpathstringyes

Request body

NameTypeRequired
namestring · ≤120yes
datasetstring · nullable
minLevelfatal | error | warn | info | debug |
messageContainsstring · nullable · ≤200
metadataMatchobject · nullable
thresholdinteger · ≤100000
windowSecinteger · ≤86400
isActiveboolean

Responses

200
Updated
400
Not a JSON object, unknown dataset, a value out of range, or no editable field sent
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, or this account is not enrolled in Majra
404
No such rule, or it belongs to another account
503
Majra is not enabled on this deployment

Response shape (200)

DELETE/alerts/{id}logs:write

Delete a log-alert rule

Parameters

NameInTypeRequired
idpathstringyes

Responses

200
Deleted
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, or this account is not enrolled in Majra
404
No such rule, or it belongs to another account
503
Majra is not enabled on this deployment

Response shape (200)

deletedboolean
idstring
GET/datasetslogs:read

List datasets

Parameters

NameInTypeRequired
pagequeryinteger
pageSizequeryinteger · ≤100

Responses

200
Page of datasets. `eventCount` is maintained by the ingest path and is approximate by design
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:read, or this account is not enrolled in Majra
429
Query rate limit exceeded (120/minute)
503
Majra is not enabled on this deployment

Response shape (200)

itemsDataset[]
totalinteger
pageinteger
pageSizeinteger
POST/datasetslogs:write

Create a dataset

Request body

NameTypeRequired
namestring · ≤64yes
labelstring · nullable · ≤120
descriptionstring · nullable · ≤2000
retentionDays7 | 30 | 90 | 180 | 365
hasPersonalDataboolean

Responses

201
Created. `retentionDays` is echoed as STORED — a deployment ceiling below the tier you asked for snaps it down, and this body is where you see that
400
Invalid JSON, zod validation error (object body), a `name` that breaks the slug rule (the message names the offending character and its position), or a `retentionDays` outside the offered tiers
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, or this account is not enrolled in Majra
409
A dataset with that name already exists on this account
413
Body exceeds 16384 bytes
429
Ingest rate limit exceeded (600/minute)
500
Dataset could not be created
503
Majra is not enabled on this deployment

Response shape (201)

datasetDataset
GET/datasets/{name}logs:read

Get one dataset

Parameters

NameInTypeRequired
namepathstringyes

Responses

200
The dataset
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:read, or this account is not enrolled in Majra
404
No such dataset, or it belongs to another account
429
Query rate limit exceeded (120/minute)
503
Majra is not enabled on this deployment

Response shape (200)

datasetDataset
PATCH/datasets/{name}logs:write

Update a dataset

Parameters

NameInTypeRequired
namepathstringyes

Request body

NameTypeRequired
labelstring · nullable · ≤120
descriptionstring · nullable · ≤2000
retentionDays7 | 30 | 90 | 180 | 365
isActiveboolean
hasPersonalDataboolean

Responses

200
Updated. `isActive: false` pauses ingestion; existing events stay readable
400
Invalid JSON, zod validation error (object body), a `retentionDays` outside the offered tiers, no editable field sent, or a `name` key — renaming is refused, not ignored
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, or this account is not enrolled in Majra
404
No such dataset, or it belongs to another account
413
Body exceeds 16384 bytes
429
Ingest rate limit exceeded (600/minute)
500
Dataset could not be updated
503
Majra is not enabled on this deployment

Response shape (200)

datasetDataset
DELETE/datasets/{name}logs:write

Delete a dataset and its events

Destroys the dataset AND every event in it (FK cascade). No soft delete, no recycle bin. `confirmEvents` must equal the server's REAL count of events about to be destroyed — the 400 hands you that number. To stop a runaway emitter without losing data, PATCH `isActive: false` instead.

Parameters

NameInTypeRequired
namepathstringyes
confirmEventsqueryintegeryes

Responses

200
Deleted, together with every event it held
400
`confirmEvents` missing or not a non-negative integer. The message carries the real count to repeat back
401
Missing, malformed, revoked or expired API key
403
Missing scope: logs:write, or this account is not enrolled in Majra
404
No such dataset, or it belongs to another account (also returned if another request deleted it first)
409
`confirmEvents` does not match the live count — your picture of the dataset is stale. The message names the real number
429
Ingest rate limit exceeded (600/minute)
500
Dataset could not be deleted
503
Majra is not enabled on this deployment

Response shape (200)

deletedboolean
namestring
eventsDeletedinteger

Public delivery

Byte-reading URLs. No key — these are what you put in a page or a message.

GET/a/{file}no key

Serve an asset (public)

served from https://2a.qa

Parameters

NameInTypeRequired
filepathstringyes

Responses

200
The bytes, inline. Cache-Control: public, max-age=31536000, immutable · CDN-Cache-Control: public, max-age=3600 · ETag is the content sha256 · Access-Control-Allow-Origin: * · Cross-Origin-Resource-Policy: cross-origin · X-Content-Type-Options: nosniff. `immutable` is honest ONLY on this shape: the id addresses a row whose bytes are content-addressed, so a re-upload of different bytes is a different id. Range IS honoured — send `Range: bytes=a-b` for a 206
206
Partial content for a single `Range: bytes=a-b` or `bytes=-n`. Content-Range names the served window and Content-Length is what was actually sent
304
`If-None-Match` matched. The ETag is the content sha256, so a match is proof the bytes are unchanged rather than a heuristic
404
Unknown id, extension does not match the stored type, asset disabled, expired, scanned infected, or the bytes are missing from disk — one response for all of them, and the body is the plain text `Not found`, NOT a JSON error object
416
Multi-range, malformed range, or a window past the end of the file. `Content-Range: bytes */<size>` names the real length so the client can re-ask
GET/a/u/{handle}/{path}no key

Serve an asset by its readable path (public)

`/a/u/<handle>/<folder>/<filename>` on the app origin, `/<handle>/<folder>/<filename>` on the dedicated asset origin. The LAST segment is the filename and the rest is the virtual folder; zero folder segments addresses an asset stored at the account root. Handle matching is case-insensitive; folder and filename are matched exactly against the stored columns. Where two assets share one (path, filename) the NEWEST wins. NOT a canonical address — a rename, a move, a handle change or a re-upload of the same name repoints it, which is why it is cached for minutes and carries `Link: <…>; rel="canonical"` naming the `/a/{file}` URL that is safe to store and share.

served from https://2a.qa

Parameters

NameInTypeRequired
handlepathstringyes
pathpathstringyes

Responses

200
The bytes, inline. Cache-Control: public, max-age=300 · CDN-Cache-Control: public, max-age=60 · Link: <canonical>; rel="canonical" · same ETag, CORS, CORP and nosniff headers as /a/{file}. NOT `immutable` — this URL is a lookup, not a content address
206
Partial content — same Range handling as /a/{file}
304
`If-None-Match` matched
404
Malformed handle, unknown handle, no asset with that folder+filename, too many or too long segments, disabled, expired, scanned infected, over quota with serving switched off, or the bytes are missing from disk — one indistinguishable `Not found` for all of them, so this URL space cannot be probed for which handles or filenames exist
416
Unsatisfiable range — same as /a/{file}

Spec & tickets

The machine-readable document itself, and management of outstanding upload tickets.

GET/ticketsassets:read

List the upload tickets this account has minted

Scoped to the ACCOUNT, not to the calling key: a ticket minted by a key that has since been revoked is precisely the one an operator needs to find. The token and its hash are NEVER returned — the token exists exactly once, in the mint response. If it was lost, mint another and revoke this one.

Parameters

NameInTypeRequired
pagequeryinteger
pageSizequeryinteger · ≤100

Responses

200
Page of tickets, newest first
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:read, or this account is not enrolled in Majra
503
Switched off on this deployment

Response shape (200)

itemsUploadTicketRow[]
totalinteger
pageinteger
pageSizeinteger
DELETE/tickets/{id}assets:write

Revoke an outstanding upload ticket

A hard delete, not a flag: a REVOKED ticket has produced nothing, so there is no `assetId` audit link to preserve, and the record that it was issued and withdrawn lives in the hash-chained activity log. An ALREADY-REDEEMED ticket is refused (409) rather than deleted, because its `assetId` is the trail from a leaked upload link to what it produced.

Parameters

NameInTypeRequired
idpathstringyes

Responses

200
Revoked
401
Missing, malformed, revoked or expired API key
403
Missing scope: assets:write, or this account is not enrolled in Majra
404
No such ticket for this account — a 404 and not a 403, so the id space cannot be probed for other accounts' tickets
409
Already redeemed, so it is already inert and is kept until its retention window closes. The body carries the `assetId` it produced
503
Switched off on this deployment

Response shape (200)

revokedboolean
idstring
GET/openapi.jsonno key

This document

Responses

200
The OpenAPI 3.1 description of this API. Cache-Control: public, max-age=300, s-maxage=300

Schemas

The named shapes the tables above refer to.

CreateLinkBody

originalUrlstring · uri
customAliasstring · ≤32
titlestring · ≤120
descriptionstring · ≤500
passwordstring · ≤60
expiresAtstring · date-time
trustPreviewboolean

IngestedAsset

idstring
urlstring · uri
originalUrlstring · uri
kindIMAGE | VIDEO | AUDIO
mimeTypeimage/png | image/jpeg | image/gif | image/webp | image/avif | video/mp4 | video/webm | audio/mpeg | audio/ogg | audio/wav
sizeinteger
widthinteger · nullable
heightinteger · nullable
durationMsinteger · nullable
pathstring · nullable
filenamestring · nullable
metadataobject · nullable
retentionExemptboolean
expiresAtstring · date-time · nullable
createdAtstring · date-time

AssetListItem

idstring
urlstring · uri
originalUrlstring · uri
kindIMAGE | VIDEO | AUDIO
mimeTypestring
sizeinteger
widthinteger · nullable
heightinteger · nullable
durationMsinteger · nullable
pathstring · nullable
filenamestring · nullable
metadataobject · nullable
sourcestring · nullable
requestsinteger
bytesServedinteger
isActiveboolean
virusScanStatuspending | clean | infected | skipped | error
retentionExemptboolean
expiresAtstring · date-time · nullable
createdAtstring · date-time

AssetDetail

idstring
urlstring · uri
kindIMAGE | VIDEO | AUDIO
mimeTypestring
sizeinteger
filenamestring · nullable
pathstring · nullable
metadataobject · nullable
requestsinteger
bytesServedinteger
isActiveboolean
virusScanStatuspending | clean | infected | skipped | error
retentionExemptboolean
expiresAtstring · date-time · nullable
createdAtstring · date-time

PatchedAsset

idstring
filenamestring · nullable
pathstring · nullable
retentionExemptboolean
expiresAtstring · date-time · nullable

UploadAssetForm

filestring · binary
filenamestring · ≤200
pathstring · ≤163
metadatastring
retentionExempttrue | false
expiresInDaysstring
sourcestring · ≤60

Base64UploadBody

base64string
filenamestring · ≤200
pathstring · ≤163
metadataobject
retentionExemptboolean
expiresInDaysinteger
sourcestring · ≤60

PatchAssetBody

filenamestring · ≤200
pathstring · ≤163
retentionExemptboolean
expiresInDaysinteger · nullable

LogEvent

idstring
levelfatal | error | warn | info | debug
messagestring
resourcestring · nullable
traceIdstring · nullable
metadataobject · nullable
timestampstring · date-time
receivedAtstring · date-time

LogEventDetail

idstring
datasetstring
levelfatal | error | warn | info | debug
messagestring
resourcestring · nullable
traceIdstring · nullable
metadataobject · nullable
apiKeyIdstring
ipstring · nullable
timestampstring · date-time
receivedAtstring · date-time

LogEventInput

levelfatal | error | warn | info | debug
messagestring · ≤8192
resourcestring · ≤128
traceIdstring · ≤128
timestampstring · date-time
metadataobject

UploadTicket

uploadUrlstring · uri
tokenstring
maxBytesinteger
allowedKindsIMAGE | VIDEO | AUDIO[]
expiresAtstring · date-time

MintTicketBody

maxBytesinteger
allowedKindsIMAGE | VIDEO | AUDIO[]
pathstring · ≤163
metadataobject
retentionExemptboolean
ttlSecinteger · ≤3600

AlertRule

idstring
namestring
datasetstring · nullable
minLevelfatal | error | warn | info | debug |
messageContainsstring · nullable
metadataMatchobject · nullable
thresholdinteger
windowSecinteger
isActiveboolean
lastFiredAtstring · date-time · nullable
createdAtstring · date-time

CreateAlertBody

namestring · ≤120
datasetstring · nullable
minLevelfatal | error | warn | info | debug |
messageContainsstring · nullable · ≤200
metadataMatchobject · nullable
thresholdinteger · ≤100000
windowSecinteger · ≤86400
isActiveboolean

Dataset

idstring
namestring · ≤64
labelstring · nullable
descriptionstring · nullable
retentionDaysinteger
isActiveboolean
hasPersonalDataboolean
eventCountinteger
lastEventAtstring · date-time · nullable
createdAtstring · date-time

CreateDatasetBody

namestring · ≤64
labelstring · nullable · ≤120
descriptionstring · nullable · ≤2000
retentionDays7 | 30 | 90 | 180 | 365
hasPersonalDataboolean

UpdateDatasetBody

labelstring · nullable · ≤120
descriptionstring · nullable · ≤2000
retentionDays7 | 30 | 90 | 180 | 365
isActiveboolean
hasPersonalDataboolean

UploadTicketRow

One minted upload credential AS LISTED. `token` and `tokenHash` are absent by design — the verifier the redeem route compares against and is never serialised — publishing it through a read scope would make it a bearer credential in its own right.

idstring
maxBytesinteger
allowedKindsstring[]
pathstring · nullable
expiresAtstring · date-time
usedAtstring · date-time · nullable
assetIdstring · nullable
createdAtstring · date-time

Error

errorstring | object
2a.qaopenapi.json