My Video Gift

API

API reference

All endpoints are under the base below, return JSON, and (except the two public discovery endpoints) require a Bearer key. Server order ids (gift_id) and share slugs are unguessable — a gift another key owns answers 404, never 403, so ids can't be probed.

API base
https://myvideogift.com/api/v1
GET/v1/occasionsPublic

The occasion catalog. Browse before you hold a key; pick a slug to pass as occasion when you create a gift.

200 OK (excerpt)
{
  "occasions": [
    {
      "slug": "anniversary-video",
      "name": "Anniversary Video",
      "short_name": "Anniversary",
      "recipient_prompt": "Who is this anniversary for?",
      "tone": "romantic",
      "answer_first": "...",
      "photo_ideas": ["...", "..."]
    }
  ]
}

Occasion fields

FieldTypeDescription
slugreqstringThe value to pass as occasion on create.
namereqstringFull occasion name.
short_namereqstringShort label.
recipient_promptreqstringHow to ask who the gift is for.
tonereqstringThe emotional register of the occasion.
gentlebooleanPresent and true only for gentle occasions (memorial, pet tribute).
sub_typesstring[]Present only when the occasion offers sub-type framing.
answer_firststringAnswer-first lead sentence; present only when the occasion has SEO content.
photo_ideasstring[]Concrete “what to gather” ideas; present only when the occasion has SEO content.
GET/v1/samplesPublic

Pre-rendered sample films — the “what you'll get” gallery that replaces a preview. Optional ?occasion=<slug> narrows the list; an unknown slug returns an empty list, not an error.

200 OK (excerpt)
{
  "caption": "Sample film · Made by our team from a demo photo set — by the same pipeline that makes yours.",
  "samples": [
    {
      "occasion_slug": "fathers-day-video",
      "video_url": "https://myvideogift.com/videos/....mp4",
      "poster_url": "https://myvideogift.com/videos/....jpg",
      "duration_sec": 71,
      "description": "..."
    }
  ]
}

caption is a required disclosure

caption is a verbatim honesty line: every sample is a team-made demo, never a customer example. If you show samples to an end user, keep it attached.

Sample fields

FieldTypeDescription
video_urlreqstringAbsolute URL to the sample MP4.
poster_urlreqstringAbsolute URL to the poster image.
descriptionreqstringHonest, verbatim description of the sample.
occasion_slugstringThe occasion this demo belongs to; omitted for generic demos.
duration_secnumberRuntime in seconds, when known.
notestringA verbatim caveat for a demo that skips a paid-provider step; usually absent.
GET/v1/accountKey required

Your key's identity and prepaid-credit balance. A cheap read for checking funds before you render.

200 OK
{
  "key_id": "ak_1a2b3c4d",
  "label": "acme-gifts",
  "mode": "live",
  "status": "active",
  "balance_cents": 5000,
  "total_spent_cents": 2500,
  "total_deliveries": 1,
  "created_at": "2026-07-08T12:00:00.000Z"
}

Account fields

FieldTypeDescription
key_idreqstringPublic key id (e.g. ak_1a2b3c4d) — safe to log.
labelreqstringYour partner label.
modereq"live" | "test"Billing mode of the key.
statusreq"active" | "revoked"A revoked key stops authenticating.
balance_centsreqnumberPrepaid credit balance, in US cents.
total_spent_centsreqnumberLifetime spend, in US cents.
total_deliveriesreqnumberLifetime render debits. Refunds do not decrement this counter.
created_atreqstringISO 8601 issuance timestamp.
POST/v1/giftsKey required

Create and reserve a gift server-side. Free — nothing is charged until you render. Send an optional Idempotency-Key header ([A-Za-z0-9._-], 1–128 chars) to make retries safe.

Request body

FieldTypeDescription
occasionreqstringA real occasion slug from GET /v1/occasions.
recipient_namereqstring1–80 chars. Who the gift is for.
relationshipreqstring1–80 chars, e.g. “wife”, “grandpa”.
memorystringUp to 500 chars. A memory that feeds the song.
story_notestringUp to 500 chars. Extra story context for the song.
feeling_wordsstring[]Up to 6 words, each 1–30 chars. Tone words for the song.
length_secnumber30–180. Target film length; defaults to the standard length.
tier"standard" | "signature"Defaults to signature. Sets the render recipe and the per-delivery price.
style"gift" | "brand" | "social"Defaults to gift (unchanged behavior). social biases delivery toward the vertical 9:16 aspect; brand carries the optional brand fields below. Recorded on the order and threaded into the song brief.
brand_logo_urlstringOptional (style brand). A public https logo URL, persisted for progressive brand rendering.
brand_colorstringOptional (style brand). An accent color as #RRGGBB, persisted for progressive brand rendering.
201 Created
{
  "gift_id": "b3f1c2a4-9e7d-4c6b-8a1f-2d5e6f7a8b9c",
  "status": "reserved",
  "tier": "signature",
  "style": "gift",
  "occasion": "anniversary-video",
  "share_slug": "maria-7x2k9q1p4m3n",
  "photo_limit": 30
}

Idempotent replay

Reusing an Idempotency-Key returns the original gift (with "idempotent_replay": true, status 200) instead of creating a second order. No price appears on create — the agent channel bills per delivery, and the consumer $49 is never surfaced here.

Errors

  • 400 invalid_json — the body was not valid JSON.
  • 400 invalid_request — a field failed validation; a truncated issues array names the offending paths.
  • 400 invalid_idempotency_key — the Idempotency-Key header was malformed.
  • 503 store_unavailable — could not persist the order; retry.
POST/v1/gifts/{gift_id}/photosKey required

Ingest photos into a gift by public URL or inline base64. Free. Up to 10 per request, up to 30 per gift, up to 4 MB each.

Request body

FieldTypeDescription
photosreqPhoto[]1–10 items. Each is either { "url": string } (≤2048 chars) or { "data": base64, "mime": "image/jpeg" | "image/png" | "image/webp" }.
subject_photo_indexnumberZero-based index into photos marking the main subject; must be in range.
URL request
{
  "photos": [
    { "url": "https://example.com/photos/wedding.jpg" },
    { "url": "https://example.com/photos/trip.jpg" }
  ],
  "subject_photo_index": 0
}
base64 request
{
  "photos": [
    { "data": "<base64-encoded-bytes>", "mime": "image/jpeg" }
  ]
}
200 OK
{
  "photo_count": 2,
  "photo_ids": ["p01", "p02"],
  "subject_photo_id": "p01"
}

URL fetches are SSRF-guarded; bytes are verified

Only public http(s) URLs are fetched — private and link-local addresses are blocked. Every image is verified to be a genuine JPEG, PNG, or WebP before storage; a HEIC or a disguised file is rejected with 415.

Errors

  • 404 not_found — the gift does not exist or is not owned by your key.
  • 409 order_locked — the gift is past the content stage and no longer accepts photos.
  • 400 invalid_request — the body failed validation (e.g. subject_photo_index out of range).
  • 400 photo_limit — the batch would exceed 30 photos; the response includes the current photo_count.
  • 400 invalid_photo_url / 400 invalid_photo_data — a URL could not be fetched (blocked, failed, too many redirects) or the base64 was empty. Partial-failure responses include the index that failed and the running photo_count.
  • 413 payload_too_large — a photo or the request body exceeded 4 MB.
  • 415 unsupported_media_type — the bytes were not a supported image.
  • 503 store_unavailable — could not store the photos; retry.
POST/v1/gifts/{gift_id}/clipsKey required

Ingest short video clips by public URL or inline base64. Free. Up to 3 clips per gift, up to 4 MB each here — larger clips (up to 500 MB) use the upload-token endpoint below.

Request body

FieldTypeDescription
clipsreqClip[]1–3 items. Each is either { "url": string } (≤2048 chars) or { "data": base64, "mime": "video/mp4" | "video/quicktime" | "video/webm" }. An optional duration_sec (≤600) may accompany either — it's a hint; the render re-probes.
URL request
{
  "clips": [
    { "url": "https://example.com/clips/first-dance.mp4", "duration_sec": 42 }
  ]
}
base64 request
{
  "clips": [
    { "data": "<base64-encoded-bytes>", "mime": "video/mp4" }
  ]
}
200 OK
{
  "clip_count": 1,
  "clip_ids": ["c01"]
}

Containers are sniffed; dimensions are re-probed

Only public http(s) URLs are fetched (SSRF-guarded, ≤4 MB). Every clip's bytes are verified to be a genuine MP4, MOV, or WebM before storage — a disguised file is rejected with 415. Dimensions and exact duration are re-probed server-side during the render, so duration_sec is an optional hint, never a requirement.

Errors

  • 404 not_found — the gift does not exist or is not owned by your key.
  • 409 order_locked — the gift is past the content stage and no longer accepts clips.
  • 400 clip_limit — the batch would exceed 3 clips; the response includes the current clip_count.
  • 400 invalid_clip_url / 400 invalid_clip_data — a URL could not be fetched (blocked, failed, too many redirects) or the base64 was empty. Partial-failure responses include the index that failed and the running clip_count.
  • 413 payload_too_large — a clip or the request body exceeded 4 MB; use the upload-token endpoint for larger clips.
  • 415 unsupported_media_type — the bytes were not a supported video container.
  • 503 store_unavailable — could not store the clips; retry.
POST/v1/gifts/{gift_id}/clips/{clip_id}Key required

Get a scoped upload token for a large video clip (up to 500 MB). Free. Drive it with @vercel/blob client upload() so the bytes stream straight to storage, never through the API.

For clips over 4 MB

Point @vercel/blob/client's upload() at this URL as its handleUploadUrl, targeting a slot clip_id (c01c03), and pass your key in headers. The token is scoped to exactly that slot; the clip meta is written automatically once the bytes land. Optional dims travel in clientPayload ({ duration_sec, width, height }) — all re-probed at render. Poll GET /v1/gifts/{id} (clip_count) to see which slots are taken.

@vercel/blob client
import { upload } from "@vercel/blob/client";

await upload("orders/b3f1c2a4-9e7d-4c6b-8a1f-2d5e6f7a8b9c/clips/c01.mp4", file, {
  access: "private",
  contentType: "video/mp4",
  handleUploadUrl: "https://myvideogift.com/api/v1/gifts/b3f1c2a4-9e7d-4c6b-8a1f-2d5e6f7a8b9c/clips/c01",
  headers: { Authorization: "Bearer mvg_live_..." },
  clientPayload: JSON.stringify({ duration_sec: 120 }),
  multipart: true,
});

Errors

  • 404 not_found — the gift does not exist or is not owned by your key.
  • 409 order_locked — the gift is past the content stage and no longer accepts clips.
  • 400 invalid_clip_id / 400 clip_limit — the slot must be c01c03.
  • 400 invalid_pathname — the upload target did not match the requested clip slot.
POST/v1/gifts/{gift_id}/renderKey requiredPaid

The one paid call. Debits the tier's wholesale price from your prepaid balance and starts the server render. Returns 202 immediately.

Request body

FieldTypeDescription
consentreqtrueMust be the literal true. Attests you hold the rights to these photos and the recipient's depiction, and that the gift is not for minors-abuse, impersonation, or harassment. Recorded for audit.
202 Accepted
{
  "status": "rendering",
  "tier": "signature",
  "charged_cents": 2500,
  "balance_cents": 2500,
  "poll_after_sec": 60
}

Render is idempotent

While a charge is live, repeating the call returns 202 with "idempotent_replay": true — it never double-charges or double-renders. If that render later fails or is held, the charge auto-refunds and the gift re-opens for a fresh render.

Errors

  • 400 consent_requiredconsent: true was missing; the message repeats the attestation.
  • 403 test_key_cannot_render — a mvg_test_ key was used to render; test keys are a free sandbox and can't render. Use a live key.
  • 402 insufficient_credits — balance below the tier price; the response includes balance_cents and price_cents. Top up, then retry.
  • 404 not_found — the gift does not exist or is not owned by your key.
  • 409 already_delivered — the film is already rendered; read the URLs from status.
  • 409 not_ready — no reservation or zero photos; the response includes photo_count.
  • 502 trigger_failed — the render could not be started after the debit; the charge is auto-refunded.
  • 503 unavailable — the render backend is not configured or a transient failure occurred.
GET/v1/gifts/{gift_id}Key required

Poll a gift's status and, once delivered, its film and gift URLs. Free. This poll is also where a held/failed render auto-refunds.

200 OK — rendering
{
  "gift_id": "b3f1c2a4-9e7d-4c6b-8a1f-2d5e6f7a8b9c",
  "status": "reserved",
  "tier": "signature",
  "occasion": "anniversary-video",
  "recipient_name": "Maria",
  "share_slug": "maria-7x2k9q1p4m3n",
  "photo_count": 2,
  "clip_count": 1,
  "created_at": "2026-07-08T12:00:00.000Z",
  "fulfillment": { "state": "running", "stage": "render" },
  "charged_cents": 2500
}
200 OK — delivered
{
  "gift_id": "b3f1c2a4-9e7d-4c6b-8a1f-2d5e6f7a8b9c",
  "status": "delivered-paid",
  "tier": "signature",
  "occasion": "anniversary-video",
  "recipient_name": "Maria",
  "share_slug": "maria-7x2k9q1p4m3n",
  "photo_count": 2,
  "clip_count": 1,
  "created_at": "2026-07-08T12:00:00.000Z",
  "fulfillment": { "state": "succeeded", "stage": "deliver" },
  "charged_cents": 2500,
  "film_url": {
    "16x9": "https://myvideogift.com/api/gift/maria-7x2k9q1p4m3n/film?aspect=16x9",
    "9x16": "https://myvideogift.com/api/gift/maria-7x2k9q1p4m3n/film?aspect=9x16"
  },
  "gift_url": "https://myvideogift.com/gift/maria-7x2k9q1p4m3n"
}

Status fields

FieldTypeDescription
gift_idreqstringThe server order id.
statusreqstringOrder status. delivered-paid means the film is ready and the delivery URLs are present.
tierreq"standard" | "signature"The render tier.
occasionreqstringThe occasion slug.
recipient_namereqstringWho the gift is for.
share_slugreqstringThe unguessable slug the delivery URLs are keyed by.
photo_countreqnumberPhotos stored on the gift.
clip_countreqnumberVideo clips stored on the gift (0–3).
created_atreqstringISO 8601 create timestamp.
fulfillmentreqobject | nullThe coarse render run: { state: "running" | "held" | "succeeded" | "failed", stage?: "prep" | "gate" | "assets" | "render" | "verify" | "deliver" }. Error detail is deliberately withheld.
charged_centsnumberPresent once a render has been charged.
refunded_centsnumberPresent once a held/failed render has been auto-refunded.
status_detailstringheld_refunded or failed_refunded when a run was auto-refunded.
billing"external"Present only for externally-billed (pay.sh) gifts.
film_urlobjectPresent only when delivered-paid: { "16x9": url, "9x16": url } — the full-HD MP4 in both aspects.
gift_urlstringPresent only when delivered-paid: the hosted gift page.

Pay per delivery, not per attempt

If a render is held (needs more/better photos) or failed, polling status auto-refunds the debit and re-opens the gift. You are only charged for a delivered film. See Errors for the refund model.

Last updated 2026-07-11.