Product
The gc.mw API: create, resolve and measure links from code
A REST API with scoped keys, an OpenAPI document, webhooks for link events, and rate limits published rather than discovered. What it does and what it deliberately does not.
By Wasim Idrishi · Updated · 4 min read
Every route in the v1 API is declared the same way: an authentication mode, a set of scopes, a zod schema for the
body, the query and the params, and a named rate limit. The declaration is the implementation. That is why the
OpenAPI document at /api/v1/openapi.json is generated rather than written, and why an undocumented field is a
build error rather than a surprise in production.
What you can do
POST /api/v1/links creates a link. The interesting fields are the ones that are not the URL: code for a custom
alias, redirectType for 301,302,307 or 308, expiresAt, maxClicks, utm, passthrough for forwarding the
path or query string onto the destination, rules for geo, device and A/B splits, and pixels.
GET /api/v1/links lists them with filters. PATCH /api/v1/links/:id edits any of it, including the destination,
which is the call most integrations actually need. GET /api/v1/links/:id/stats returns the time series.
POST /api/v1/links/bulk takes a batch. GET /api/v1/links/export streams every link and its destination as CSV,
which exists so that leaving is a supported operation rather than a support ticket.
Domains, names, bio pages, webhooks and API keys each have their own resource. POST /api/v1/public/shorten is the
one unauthenticated endpoint, the homepage box, and it is limited to ten links per IP per hour and produces
30-day links.
A worked example
A ticketing platform generates one short link per order confirmation.
curl -X POST https://gc.mw/api/v1/links \
-H "Authorization: Bearer gc_live_a1b2c3..." \
-H "Content-Type: application/json" \
-d '{"url":"https://tickets.example.com/o/8842","code":"t8842","utm":{"source":"email","medium":"confirmation"}}'
Back comes gc.mw/t8842 with its id, its QR endpoints and its click count. The email template embeds
gc.mw/t8842.qr as the gate-scan image, which means the venue scans the same link the customer clicks, and the two
never blur together because the QR endpoint tags its traffic as a scan.
When the event moves rooms, one PATCH re-points every unclaimed ticket in that batch. The redirect cache
invalidates on write, so the change reaches the door staff in about a second rather than at the next deploy.
Webhooks, so you do not poll
Eight events fire today: link.created, link.updated, link.deleted, link.claimed, link.expired,
link.blocked and domain.active, plus report.created for abuse workflows. Ten endpoints per workspace, each with
its own signing secret that you can rotate without downtime.
link.blocked is the one to wire up first. If a destination you own gets flagged by a safety provider, that webhook
is how your on-call finds out before your customers do.
What the API deliberately will not do
It will not resolve arbitrary third-party links for you at scale. The expander endpoint is rate limited to twenty calls a minute, follows at most three hops, and refuses targets that resolve to private addresses. Building a crawler on it is not a supported use.
It will not let you take a workspace id from a request body. Every query is scoped to the workspace the key belongs to, server-side. There is no admin parameter that widens it.
It will not return raw documents. Every response is an explicit DTO. Password hashes, claim tokens, verification tokens and other people's email addresses have no code path to the wire, which is a duller guarantee than a feature list and a more useful one.
It will not accept an alias we have reserved. Roughly a hundred platform paths, pricing, blog, tools,
login, plus a brand-protected list covering names like paypal, whatsapp and hdfc are refused at creation
with a 422 naming the reason. Automated pipelines hit this most often when they slugify a page title, so handle the
error rather than retrying the same string.
It will not tell you a code exists. A lookup on a code you do not own returns the same response whether the code is free or belongs to someone else, and enumeration attempts are rate limited. Guessing other people's codes in bulk is treated as an attack, not as capacity planning.
Before you write the integration
Bulk URL Shortener
Open the full toolIf the job is one spreadsheet of links rather than an ongoing integration, the bulk tool does it in a browser with a free account and hands you a CSV. Plenty of teams reach for an API key and then discover they needed it once.
Read the plan limits on the pricing page before you design around a call volume, and if the links are going to carry your name, set up the domain first on the custom domain links page, moving links between hosts later is possible, but it means every already-printed code points at the old one.
Questions people ask
How do I authenticate?
A workspace API key in the Authorization header. Keys are shown once at creation and only their SHA-256 hash is stored, so a leaked key cannot be recovered from our database, it can only be rotated. Each key carries scopes: links:read, links:write, analytics:read, domains:read, domains:write, names:write, pages:write and billing:read.
What are the rate limits?
Pro allows 10,000 API calls a day, Business and Team 100,000. On top of the daily quota there is a burst limit of 600 requests a minute per key, and link creation is separately capped at 60 a minute. Exceeding a limit returns 429 with the window in the response, never a silent drop.
Is there an OpenAPI document?
Yes, generated from the same zod schemas the routes validate with, at /api/v1/openapi.json. It cannot drift from the implementation because there is no second definition to drift from.
Can I use the API on the free plan?
No. Free accounts get zero API calls a day. This is a spam decision rather than a pricing one: an unauthenticated, unmetered creation endpoint is how a shortener ends up on a block list.
How do webhooks retry?
Eight attempts over 24 hours, 30 seconds, 2 minutes, 10 minutes, 1 hour, 3 hours, 6 hours, 12 hours, 24 hours. Each delivery is signed, the request times out after 8 seconds, and the delivery log in the dashboard shows the response body we got back.