Endpoint reference
The single call the site script makes: a batch of destinations in, a short link each out. Documented for anyone building pages the script cannot reach — server-rendered HTML, a native app, a static build step.
On this page
The call
Addresses in, a short link each out. It is the only endpoint a site key opens, and the only request the site script ever makes.
POSTmethodhttps://montvo.com/api/v1/site/linksAuthorizationrequiredBearer pk_live_…— your site key, from the Developers screen. A secret key is refused here.Content-Typeexpectedapplication/json
curl -X POST https://montvo.com/api/v1/site/links \ -H "Authorization: Bearer pk_live_9f2c4b8a" \ -H "Content-Type: application/json" \ -d '{ "urls": [ "https://example.com/download", "https://store.example.com/hoodie" ] }'Use your site key (pk_live_…). Made to be public: it goes into your page and can only create links.
Both are on Developers in the dashboard · Which key when
The request body
urlsrequiredstring[]- Between one and 50 web addresses. Anything in the list that is not a string, or is not an address we can shorten, is dropped quietly rather than failing the whole call — the answer tells you which ones survived.
Addresses are normalized and deduplicated before anything is created, so sending the same destination twice in one call, or written two different ways, produces one link.
What will not be shortened
- Anything that is not
httporhttps. - Anything on
montvo.comormontvo.link, or a subdomain of either. A Montvo link cannot point at Montvo. - Anything that does not parse as a web address at all.
- An address whose derived slug is already taken. That one is skipped; the rest of the call is unaffected.
The response
200, with one object: links, keyed by the address exactly as you sent it.
{ "links": { "https://example.com/download": "https://montvo.link/105010", "https://store.example.com/hoodie": "https://montvo.link/1gjybr" }}Keyed by what you sent
An address that could not be shortened is simply absent from links. Read it with a fallback to the original rather than assuming every key you sent comes back.
Errors
One shape for every failure, so a caller writes one branch rather than one per case:
{ "error": { "code": "too_many_urls", "message": "Send at most 50 addresses at a time." }}| Status | Code | What happened |
|---|---|---|
400 | invalid_body | The body is not JSON, or is not a JSON object. |
401 | unauthorized | No Authorization header, or a key that is not a live site key. The same answer either way, on purpose: a key found in a public page should not be able to map out which endpoints exist. |
422 | invalid_urls | urls is missing, empty, or not an array — or nothing in it was an address we can shorten. |
422 | too_many_urls | More than 50 addresses. Split the list and call twice. |
429 | rate_limited | Over the limit for this minute. Retry-After says how many seconds to wait. |
500 | server_error | Ours. Nothing was created; retrying is safe. |
Every call, including the ones that failed, shows up in the request log on your Developers screen with its status and how long it took. The log keeps 7 days.
Rate limit
3,000 calls a minute, per site key. That ceiling is set by your traffic rather than by your code — a site key is called once by every visitor who lands on a page — so it is deliberately far above the secret key's, and it is counted separately. A busy afternoon is not supposed to look like an attack.
X-RateLimit-Limit- On every answer to a key we recognized — a
401carries none of these. 3,000 for a site key. X-RateLimit-Remaining- Calls left in the current minute.
Retry-After429 only- Seconds until the window resets.
Calling it from a browser
Any origin may call this endpoint. That is the point of a tag on somebody else's page, and the reason the site key is a different kind of key from the secret one: it opens nothing worth stealing.
- The answer carries
Access-Control-Allow-Originfor the calling origin, andVary: Origin. OPTIONSis answered with204and a 24-hourAccess-Control-Max-Age, so the preflight costs a visitor one request rather than one per page.AuthorizationandContent-Typeare the allowed headers;POSTandOPTIONSthe allowed methods.
Or just use the tag
Calling it from a server
A page rendered on a server has no anchors for a browser script to find, so the swap has to happen while the HTML is being built. Ask for the short links, then write them into the markup:
const urls = [ "https://example.com/download", "https://store.example.com/hoodie",]; const res = await fetch("https://montvo.com/api/v1/site/links", { method: "POST", headers: { Authorization: `Bearer ${process.env.MONTVO_SITE_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ urls }),}); // An address we could not shorten is simply absent from the// answer, so fall back to the original rather than to undefined.const { links } = await res.json();const paid = (url) => links[url] ?? url;Use your site key (pk_live_…). Made to be public: it goes into your page and can only create links.
Both are on Developers in the dashboard · Which key when
Cache what comes back. A short link for a given destination does not change, and asking again on every render spends your rate limit on an answer you already have.
Server-side is a different exposure