Deliver your customers' videos and exports from your own site: list what a customer owns, mint short-lived download links from your server with a standard API key, and get told the moment a purchase completes or an export is ready with signed webhooks or a polling feed.
First published version: server-to-server endpoints to list a customer's videos and exports and to get one-hour download links, authenticated with a standard API key.
Your customers use the embed, but you may want them to find and download their videos from your own account pages. This server-to-server API lets your backend list what a customer owns and exported, and fetch a short-lived download link for any file. Your customers never see our site, and your API key never reaches a browser.
Base URL: https://musicvideomarketplace.com/api/public/partner/v1.
Create a standard API key in your partner dashboard (Credentials → API keys). Send it as a bearer token from your server:
Authorization: Bearer mvm_live_xxxxxxxxxxxxxxxxxxxxxxxxfulfilment:read scope to list and fulfilment:download to get links. Keys created from 14 October 2026 have both. Older standard keys do not: create a new one.403 wrong_key_type. A revoked key returns 401 revoked_api_key.429 rate_limited with Retry-After.Customers are identified by the same partner_external_user_id you send in the signed embed payload. A customer exists here from the moment they first open your embed. Before that, calls return 404 user_not_found.
GET https://musicvideomarketplace.com/api/public/partner/v1/users/{external_user_id}/videos?limit=50&offset=0URL-encode the user ID. limit is 1 to 100 (default 50); results are newest first. NewAdd since (an ISO 8601 date or date-time, for example since=2026-10-29T09:30:00Z) to get only the videos this customer acquired, or exported something from, since then. total counts only those. An invalid since returns 400 invalid_since.
{
"external_user_id": "your-user-123",
"total": 2,
"limit": 50,
"offset": 0,
"videos": [
{
"video_id": "0b6f2c1e-6c1a-4a53-9a4f-3f0f0f0f0f0f",
"title": "Sunset Vibes",
"artist": "Example Artist",
"thumbnail_url": "https://…",
"duration_seconds": 240,
"acquired_at": "2026-10-02T14:21:07Z",
"source": "partner",
"downloads": { "original": true, "reel": true, "bts": false },
"exports": [
{
"export_id": "6d3a5b1c-2b7e-4c0a-8f55-9a1d3c7e2b10",
"name": "My edit",
"created_at": "2026-10-03T09:12:44Z",
"files": ["main"]
},
{
"export_id": "b21e7f40-93cd-4f53-8c1a-0e6a5d2b7c11",
"name": "My edit",
"created_at": "2026-10-03T09:12:51Z",
"files": ["social_reel"]
}
]
}
]
}| Field | Meaning |
|---|---|
source | Always partner: the video was bought or claimed through your integration. Videos a customer bought elsewhere on musicvideomarketplace.com are not part of this API. |
downloads | Which original files this customer can download: the music video, the vertical reel, and behind-the-scenes. false means not owned or no file exists. |
exports[] | Everything this customer has exported from the editor for that video, newest first, including every deliverable. Each rendered file is its own entry with its own export_id. Only finished files are listed. |
exports[].files | The kind of file: main (music video), reel, lyric, social_canvas (Spotify Canvas and other loop formats), social_reel (vertical reel and clips), social, final, bts (behind the scenes). |
POST https://musicvideomarketplace.com/api/public/partner/v1/downloads
Content-Type: application/json
{
"external_user_id": "your-user-123",
"video_id": "0b6f2c1e-6c1a-4a53-9a4f-3f0f0f0f0f0f",
"kind": "original"
}| Field | Required | Description |
|---|---|---|
external_user_id | Yes | The customer. |
video_id | Yes | From the list response. |
kind | No (default original) | original, reel, bts or export. |
export_id | When kind is export | From exports[].export_id. |
file | No | For exports. Normally leave it out, because the export_id already names one file. If you send it, it must match the kind listed for that export, otherwise you get 404 file_not_available. |
{
"download_url": "https://…",
"filename": "Sunset_Vibes.mp4",
"expires_in": 3600,
"kind": "original"
}409 export_not_ready: try again shortly. Only finished files appear in the list.const BASE = 'https://musicvideomarketplace.com/api/public/partner/v1';
const headers = { Authorization: `Bearer ${process.env.MVM_API_KEY}`, 'Content-Type': 'application/json' };
// Your "My videos" page
async function myVideos(userId) {
const res = await fetch(`${BASE}/users/${encodeURIComponent(userId)}/videos`, { headers });
if (res.status === 404) return []; // the customer has not used the embed yet
if (!res.ok) throw new Error((await res.json()).error.message);
return (await res.json()).videos;
}
// Your "Download" button handler: send the customer straight to a fresh link
async function downloadUrl(userId, videoId) {
const res = await fetch(`${BASE}/downloads`, {
method: 'POST',
headers,
body: JSON.stringify({ external_user_id: userId, video_id: videoId, kind: 'original' }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
return (await res.json()).download_url;
}Errors use one shape: { "error": { "code": "...", "message": "..." } }.
| Status | code | Cause |
|---|---|---|
| 400 | invalid_request, invalid_json | Missing or malformed fields. |
| 400 | invalid_since, invalid_type | since is not an ISO 8601 date, or type is not an event type (sections 3 and 8). |
| 401 | missing_api_key, invalid_api_key, revoked_api_key | Key missing, unknown or revoked. |
| 403 | wrong_key_type, missing_scope, partner_inactive | Not a standard key, no fulfilment scope, or your partner account is not active. |
| 403 | not_owned | The customer does not own that video or file. |
| 403 | not_available_in_test | A test key asked for an original, reel or behind-the-scenes file. Only test exports are available in test mode. |
| 404 | user_not_found | No customer with that ID has opened your embed. |
| 404 | export_not_found, file_not_available, video_unavailable | The export, file or video does not exist. |
| 429 | rate_limited | Too many requests. Retry after the Retry-After header. |
| 500 | internal_error | Retry shortly. |
An API key labelled test works against your test customers only: the customers who signed in to the embed with your test client ID (see the partner embed guide, section 14). Pass the same partner_external_user_id you signed with. Responses include "test": true.
downloads is always false for original, reel and behind-the-scenes: full-quality files are never available in test mode.kind: "export". Asking for an original returns 403 not_available_in_test.You do not have to keep asking whether something has happened. We can post a signed message to your server the moment it does, and the same events are always available as a feed you can read on your own schedule.
| type | When | data |
|---|---|---|
purchase.completed | A customer now has a video through your integration (bought on your checkout, bought in the embed, or claimed), or has bought an add-on. | video_id, order_ref, source (partner_pays, mvm, claim or addon), acquired_at, and addon for an add-on. |
export.ready | A file the customer exported from the editor has finished and can be downloaded. | video_id, export_id, kind (as in section 3), created_at. |
ping | Only when you press Send test event on your dashboard. | message. |
Every event has the same envelope. Use external_user_id to find the customer on your side, then call section 3 or 4 for the files.
{
"id": "5f0c2c52-3d0b-4d3e-9d0c-6a6a2f9d1b11",
"type": "export.ready",
"created_at": "2026-10-29T10:42:11.482Z",
"test": false,
"external_user_id": "your-user-123",
"data": {
"video_id": "0b6f2c1e-6c1a-4a53-9a4f-3f0f0f0f0f0f",
"export_id": "6d3a5b1c-2b7e-4c0a-8f55-9a1d3c7e2b10",
"kind": "main",
"created_at": "2026-10-29T10:41:58Z"
}
}whsec_) and store it as an environment variable on your server.The address must be https on the standard port, on the public internet. Localhost, internal names and private network addresses are refused. We do not follow redirects.
Every request carries X-MVM-Signature: t=<unix seconds>,v1=<hex> and X-MVM-Event-Id. The signature is an HMAC-SHA256 of "<t>.<raw request body>" using your signing secret. Compute it over the raw body (before any JSON parsing), compare in constant time, and reject anything where t is more than 5 minutes old so a captured request cannot be replayed.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Keep the raw body: the signature is over exactly what we sent.
app.post('/webhooks/mvm', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-MVM-Signature') || '';
const t = /t=(\d+)/.exec(header)?.[1];
const v1 = /v1=([0-9a-f]+)/.exec(header)?.[1];
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', process.env.MVM_WEBHOOK_SECRET)
.update(`${t}.${req.body.toString('utf8')}`).digest('hex');
const ok = expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// event.id is the same on every retry: skip ids you have already handled.
handleEvent(event); // keep this quick, or queue it
res.sendStatus(200);
});id never changes between retries: keep the ids you have handled and ignore repeats.If you would rather poll, or you need to catch up after downtime, the same events are available for 30 days:
GET https://musicvideomarketplace.com/api/public/partner/v1/events?since=2026-10-29T09:30:00Z&limit=100&type=export.readyEvents come oldest first, for your key's environment only (a test key sees test events). limit is 1 to 500 (default 100); typeis optional. Save next_since and send it as since next time to carry on exactly where you stopped. With no new events next_since is the since you sent. Dedupe by id, as with webhooks.
{
"test": false,
"events": [ { "id": "…", "type": "purchase.completed", "created_at": "2026-10-29T10:02:41.120Z", "test": false, "external_user_id": "your-user-123", "data": { "video_id": "…", "order_ref": "ord_8841", "source": "partner_pays", "acquired_at": "2026-10-29T10:02:41Z" } } ],
"next_since": "2026-10-29T10:02:41.120Z"
}The feed uses the same standard key and fulfilment:read scope as section 3.
Questions: info@musicvideomarketplace.com. Support is staffed Monday–Friday, 9–5 UK time.