[{"data":1,"prerenderedAt":806},["ShallowReactive",2],{"content-query-j1UbSDGOfV":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"crumb":11,"term":11,"summary":12,"group":13,"order":14,"updated":15,"cta":16,"related":24,"faq":43,"body":56,"_type":799,"_id":800,"_source":801,"_file":802,"_stem":803,"_extension":804,"sitemap":805},"/glossary/callback-url","glossary",false,"","What is a callback URL? Callback URL vs webhook URL","A callback URL is an address you give a service so it can call you back later. How callback URLs work in OAuth, payments and async APIs, and how they differ from webhook URLs.","reference","Callback URL","An address a service calls you back on: OAuth redirects, async jobs, webhooks.","Basics",3,"2026-10-28",{"eyebrow":17,"title":18,"text":19,"to":20,"label":21,"secondaryTo":22,"secondaryLabel":23},"Webhook Relay","Test a callback on localhost","Server-to-server callbacks need a public URL. Webhook Relay gives you a permanent HTTPS URL that forwards each callback to localhost or a private server, with logs of every request.","/docs/webhooks/internal/localhost/","Receive callbacks locally","/webhook-bin/","Inspect a callback",[25,28,31,34,37,40],{"title":26,"to":27},"Webhook URL","/glossary/webhook-url/",{"title":29,"to":30},"Webhook endpoint","/glossary/webhook-endpoint/",{"title":32,"to":33},"Webhook listener","/glossary/webhook-listener/",{"title":35,"to":36},"Tunnel","/glossary/tunnel/",{"title":38,"to":39},"localhost:3000","/localhost/3000/",{"title":41,"to":42},"Webhooks vs APIs","/blog/webhooks-vs-api/",[44,47,50,53],{"q":45,"a":46},"What is a callback URL?","A callback URL is an address you give to another service so it can call you back when something finishes or happens. The service later sends a request to that URL, either by redirecting the user's browser to it, as in OAuth login, or by making a server-to-server HTTP request, as with webhooks and async job notifications.",{"q":48,"a":49},"What is the difference between a callback URL and a webhook URL?","A webhook URL is one kind of callback URL. Webhook URLs are registered once and receive a server-to-server request for every matching event. Callback URLs are often passed per request or per flow, and in OAuth the callback is a browser redirect that carries an authorization code rather than an event payload.",{"q":51,"a":52},"What is an OAuth callback URL?","It is the redirect URI the authorization server sends the user's browser back to after they log in and grant access. The URL receives an authorization code and a state value in the query string, and your app exchanges the code for tokens. It must exactly match one of the redirect URIs registered with the provider.",{"q":54,"a":55},"Can a callback URL be localhost?","For OAuth browser redirects, often yes, because the user's own browser follows the redirect and can reach localhost. Many providers allow http://localhost redirect URIs for development. For server-to-server callbacks, no: the provider's servers cannot reach localhost, so you need a public URL that forwards to your machine.",{"type":57,"children":58,"toc":792},"root",[59,73,83,88,95,232,237,243,254,313,318,324,384,619,625,752,758,786],{"type":60,"tag":61,"props":62,"children":63},"element","p",{},[64,71],{"type":60,"tag":65,"props":66,"children":67},"strong",{},[68],{"type":69,"value":70},"text","A callback URL is an address you give to another service so it can call you back later, when something has finished or happened.",{"type":69,"value":72}," Instead of you polling for a result, the service sends a request to your callback URL. Depending on the context, that request is a browser redirect (OAuth login, payment checkout) or a server-to-server HTTP call (webhooks, async job results).",{"type":60,"tag":74,"props":75,"children":77},"pre",{"code":76},"https://app.example.com/auth/callback?code=4/0AbCD...&state=xyz123\n",[78],{"type":60,"tag":79,"props":80,"children":81},"code",{"__ignoreMap":7},[82],{"type":69,"value":76},{"type":60,"tag":61,"props":84,"children":85},{},[86],{"type":69,"value":87},"That's an OAuth callback: the provider sent the user's browser back to your app with an authorization code.",{"type":60,"tag":89,"props":90,"children":92},"h2",{"id":91},"where-callback-urls-show-up",[93],{"type":69,"value":94},"Where callback URLs show up",{"type":60,"tag":96,"props":97,"children":98},"table",{},[99,123],{"type":60,"tag":100,"props":101,"children":102},"thead",{},[103],{"type":60,"tag":104,"props":105,"children":106},"tr",{},[107,113,118],{"type":60,"tag":108,"props":109,"children":110},"th",{},[111],{"type":69,"value":112},"Context",{"type":60,"tag":108,"props":114,"children":115},{},[116],{"type":69,"value":117},"Who calls it",{"type":60,"tag":108,"props":119,"children":120},{},[121],{"type":69,"value":122},"What arrives",{"type":60,"tag":124,"props":125,"children":126},"tbody",{},[127,159,176,202],{"type":60,"tag":104,"props":128,"children":129},{},[130,136,141],{"type":60,"tag":131,"props":132,"children":133},"td",{},[134],{"type":69,"value":135},"OAuth / OpenID Connect login",{"type":60,"tag":131,"props":137,"children":138},{},[139],{"type":69,"value":140},"The user's browser, via a redirect",{"type":60,"tag":131,"props":142,"children":143},{},[144,149,151,157],{"type":60,"tag":79,"props":145,"children":147},{"className":146},[],[148],{"type":69,"value":79},{"type":69,"value":150}," and ",{"type":60,"tag":79,"props":152,"children":154},{"className":153},[],[155],{"type":69,"value":156},"state",{"type":69,"value":158}," in the query string",{"type":60,"tag":104,"props":160,"children":161},{},[162,167,171],{"type":60,"tag":131,"props":163,"children":164},{},[165],{"type":69,"value":166},"Payment checkout return",{"type":60,"tag":131,"props":168,"children":169},{},[170],{"type":69,"value":140},{"type":60,"tag":131,"props":172,"children":173},{},[174],{"type":69,"value":175},"An order or session ID",{"type":60,"tag":104,"props":177,"children":178},{},[179,184,189],{"type":60,"tag":131,"props":180,"children":181},{},[182],{"type":69,"value":183},"Async API job (transcription, video, SMS status)",{"type":60,"tag":131,"props":185,"children":186},{},[187],{"type":69,"value":188},"The provider's server",{"type":60,"tag":131,"props":190,"children":191},{},[192,194,200],{"type":69,"value":193},"A ",{"type":60,"tag":79,"props":195,"children":197},{"className":196},[],[198],{"type":69,"value":199},"POST",{"type":69,"value":201}," with the job result",{"type":60,"tag":104,"props":203,"children":204},{},[205,210,214],{"type":60,"tag":131,"props":206,"children":207},{},[208],{"type":69,"value":209},"Webhooks",{"type":60,"tag":131,"props":211,"children":212},{},[213],{"type":69,"value":188},{"type":60,"tag":131,"props":215,"children":216},{},[217,218,223,225],{"type":69,"value":193},{"type":60,"tag":79,"props":219,"children":221},{"className":220},[],[222],{"type":69,"value":199},{"type":69,"value":224}," with an event ",{"type":60,"tag":226,"props":227,"children":229},"a",{"href":228},"/glossary/webhook-payload/",[230],{"type":69,"value":231},"payload",{"type":60,"tag":61,"props":233,"children":234},{},[235],{"type":69,"value":236},"The name is borrowed from programming, where a callback is a function you pass to code that will call it later. A callback URL is the same idea across the network.",{"type":60,"tag":89,"props":238,"children":240},{"id":239},"callback-url-vs-webhook-url",[241],{"type":69,"value":242},"Callback URL vs webhook URL",{"type":60,"tag":61,"props":244,"children":245},{},[246,247,252],{"type":69,"value":193},{"type":60,"tag":226,"props":248,"children":249},{"href":27},[250],{"type":69,"value":251},"webhook URL",{"type":69,"value":253}," is a specific kind of callback URL:",{"type":60,"tag":255,"props":256,"children":257},"ul",{},[258,285,295],{"type":60,"tag":259,"props":260,"children":261},"li",{},[262,267,269,275,277,283],{"type":60,"tag":65,"props":263,"children":264},{},[265],{"type":69,"value":266},"Registration.",{"type":69,"value":268}," A webhook URL is registered once, in a dashboard or by API, and receives every matching event until you remove it. Callback URLs are often passed per request: Twilio's ",{"type":60,"tag":79,"props":270,"children":272},{"className":271},[],[273],{"type":69,"value":274},"StatusCallback",{"type":69,"value":276}," parameter, or the ",{"type":60,"tag":79,"props":278,"children":280},{"className":279},[],[281],{"type":69,"value":282},"redirect_uri",{"type":69,"value":284}," in each OAuth authorization request.",{"type":60,"tag":259,"props":286,"children":287},{},[288,293],{"type":60,"tag":65,"props":289,"children":290},{},[291],{"type":69,"value":292},"Transport.",{"type":69,"value":294}," Webhooks are always server-to-server. OAuth and checkout callbacks go through the user's browser.",{"type":60,"tag":259,"props":296,"children":297},{},[298,303,305,311],{"type":60,"tag":65,"props":299,"children":300},{},[301],{"type":69,"value":302},"Content.",{"type":69,"value":304}," A webhook carries an event payload and usually a ",{"type":60,"tag":226,"props":306,"children":308},{"href":307},"/glossary/webhook-signature/",[309],{"type":69,"value":310},"signature",{"type":69,"value":312},". An OAuth callback carries a short-lived code that is only useful together with your client secret.",{"type":60,"tag":61,"props":314,"children":315},{},[316],{"type":69,"value":317},"In casual use the two terms overlap. Some providers label the webhook setting \"callback URL\", so check the docs for whether the provider will redirect a browser or send a request from its servers.",{"type":60,"tag":89,"props":319,"children":321},{"id":320},"how-an-oauth-callback-url-works",[322],{"type":69,"value":323},"How an OAuth callback URL works",{"type":60,"tag":325,"props":326,"children":327},"ol",{},[328,348,353,365],{"type":60,"tag":259,"props":329,"children":330},{},[331,333,339,341,346],{"type":69,"value":332},"Your app sends the user to the provider with ",{"type":60,"tag":79,"props":334,"children":336},{"className":335},[],[337],{"type":69,"value":338},"redirect_uri=https://app.example.com/auth/callback",{"type":69,"value":340}," and a random ",{"type":60,"tag":79,"props":342,"children":344},{"className":343},[],[345],{"type":69,"value":156},{"type":69,"value":347},".",{"type":60,"tag":259,"props":349,"children":350},{},[351],{"type":69,"value":352},"The user logs in and approves access.",{"type":60,"tag":259,"props":354,"children":355},{},[356,358,364],{"type":69,"value":357},"The provider redirects the browser to your callback URL with ",{"type":60,"tag":79,"props":359,"children":361},{"className":360},[],[362],{"type":69,"value":363},"?code=...&state=...",{"type":69,"value":347},{"type":60,"tag":259,"props":366,"children":367},{},[368,370,375,377,382],{"type":69,"value":369},"Your server checks that ",{"type":60,"tag":79,"props":371,"children":373},{"className":372},[],[374],{"type":69,"value":156},{"type":69,"value":376}," matches what it issued, then exchanges the ",{"type":60,"tag":79,"props":378,"children":380},{"className":379},[],[381],{"type":69,"value":79},{"type":69,"value":383}," for tokens in a back-channel request.",{"type":60,"tag":74,"props":385,"children":389},{"code":386,"language":387,"meta":7,"className":388,"style":7},"app.get('/auth/callback', async (req, res) => {\n  if (req.query.state !== req.session.oauthState) return res.sendStatus(400)\n  const tokens = await exchangeCodeForTokens(req.query.code)\n  req.session.user = await fetchProfile(tokens.access_token)\n  res.redirect('/dashboard')\n})\n","js","language-js shiki shiki-themes github-dark",[390],{"type":60,"tag":79,"props":391,"children":392},{"__ignoreMap":7},[393,468,522,555,583,610],{"type":60,"tag":394,"props":395,"children":398},"span",{"class":396,"line":397},"line",1,[399,405,411,416,422,427,433,438,444,448,453,458,463],{"type":60,"tag":394,"props":400,"children":402},{"style":401},"--shiki-default:#E1E4E8",[403],{"type":69,"value":404},"app.",{"type":60,"tag":394,"props":406,"children":408},{"style":407},"--shiki-default:#B392F0",[409],{"type":69,"value":410},"get",{"type":60,"tag":394,"props":412,"children":413},{"style":401},[414],{"type":69,"value":415},"(",{"type":60,"tag":394,"props":417,"children":419},{"style":418},"--shiki-default:#9ECBFF",[420],{"type":69,"value":421},"'/auth/callback'",{"type":60,"tag":394,"props":423,"children":424},{"style":401},[425],{"type":69,"value":426},", ",{"type":60,"tag":394,"props":428,"children":430},{"style":429},"--shiki-default:#F97583",[431],{"type":69,"value":432},"async",{"type":60,"tag":394,"props":434,"children":435},{"style":401},[436],{"type":69,"value":437}," (",{"type":60,"tag":394,"props":439,"children":441},{"style":440},"--shiki-default:#FFAB70",[442],{"type":69,"value":443},"req",{"type":60,"tag":394,"props":445,"children":446},{"style":401},[447],{"type":69,"value":426},{"type":60,"tag":394,"props":449,"children":450},{"style":440},[451],{"type":69,"value":452},"res",{"type":60,"tag":394,"props":454,"children":455},{"style":401},[456],{"type":69,"value":457},") ",{"type":60,"tag":394,"props":459,"children":460},{"style":429},[461],{"type":69,"value":462},"=>",{"type":60,"tag":394,"props":464,"children":465},{"style":401},[466],{"type":69,"value":467}," {\n",{"type":60,"tag":394,"props":469,"children":471},{"class":396,"line":470},2,[472,477,482,487,492,497,502,507,511,517],{"type":60,"tag":394,"props":473,"children":474},{"style":429},[475],{"type":69,"value":476},"  if",{"type":60,"tag":394,"props":478,"children":479},{"style":401},[480],{"type":69,"value":481}," (req.query.state ",{"type":60,"tag":394,"props":483,"children":484},{"style":429},[485],{"type":69,"value":486},"!==",{"type":60,"tag":394,"props":488,"children":489},{"style":401},[490],{"type":69,"value":491}," req.session.oauthState) ",{"type":60,"tag":394,"props":493,"children":494},{"style":429},[495],{"type":69,"value":496},"return",{"type":60,"tag":394,"props":498,"children":499},{"style":401},[500],{"type":69,"value":501}," res.",{"type":60,"tag":394,"props":503,"children":504},{"style":407},[505],{"type":69,"value":506},"sendStatus",{"type":60,"tag":394,"props":508,"children":509},{"style":401},[510],{"type":69,"value":415},{"type":60,"tag":394,"props":512,"children":514},{"style":513},"--shiki-default:#79B8FF",[515],{"type":69,"value":516},"400",{"type":60,"tag":394,"props":518,"children":519},{"style":401},[520],{"type":69,"value":521},")\n",{"type":60,"tag":394,"props":523,"children":524},{"class":396,"line":14},[525,530,535,540,545,550],{"type":60,"tag":394,"props":526,"children":527},{"style":429},[528],{"type":69,"value":529},"  const",{"type":60,"tag":394,"props":531,"children":532},{"style":513},[533],{"type":69,"value":534}," tokens",{"type":60,"tag":394,"props":536,"children":537},{"style":429},[538],{"type":69,"value":539}," =",{"type":60,"tag":394,"props":541,"children":542},{"style":429},[543],{"type":69,"value":544}," await",{"type":60,"tag":394,"props":546,"children":547},{"style":407},[548],{"type":69,"value":549}," exchangeCodeForTokens",{"type":60,"tag":394,"props":551,"children":552},{"style":401},[553],{"type":69,"value":554},"(req.query.code)\n",{"type":60,"tag":394,"props":556,"children":558},{"class":396,"line":557},4,[559,564,569,573,578],{"type":60,"tag":394,"props":560,"children":561},{"style":401},[562],{"type":69,"value":563},"  req.session.user ",{"type":60,"tag":394,"props":565,"children":566},{"style":429},[567],{"type":69,"value":568},"=",{"type":60,"tag":394,"props":570,"children":571},{"style":429},[572],{"type":69,"value":544},{"type":60,"tag":394,"props":574,"children":575},{"style":407},[576],{"type":69,"value":577}," fetchProfile",{"type":60,"tag":394,"props":579,"children":580},{"style":401},[581],{"type":69,"value":582},"(tokens.access_token)\n",{"type":60,"tag":394,"props":584,"children":586},{"class":396,"line":585},5,[587,592,597,601,606],{"type":60,"tag":394,"props":588,"children":589},{"style":401},[590],{"type":69,"value":591},"  res.",{"type":60,"tag":394,"props":593,"children":594},{"style":407},[595],{"type":69,"value":596},"redirect",{"type":60,"tag":394,"props":598,"children":599},{"style":401},[600],{"type":69,"value":415},{"type":60,"tag":394,"props":602,"children":603},{"style":418},[604],{"type":69,"value":605},"'/dashboard'",{"type":60,"tag":394,"props":607,"children":608},{"style":401},[609],{"type":69,"value":521},{"type":60,"tag":394,"props":611,"children":613},{"class":396,"line":612},6,[614],{"type":60,"tag":394,"props":615,"children":616},{"style":401},[617],{"type":69,"value":618},"})\n",{"type":60,"tag":89,"props":620,"children":622},{"id":621},"common-mistakes",[623],{"type":69,"value":624},"Common mistakes",{"type":60,"tag":255,"props":626,"children":627},{},[628,658,675,717,727],{"type":60,"tag":259,"props":629,"children":630},{},[631,641,643,649,650,656],{"type":60,"tag":65,"props":632,"children":633},{},[634,640],{"type":60,"tag":79,"props":635,"children":637},{"className":636},[],[638],{"type":69,"value":639},"redirect_uri_mismatch",{"type":69,"value":347},{"type":69,"value":642}," OAuth providers compare the callback URL exactly: scheme, host, port, path and often the trailing slash. ",{"type":60,"tag":79,"props":644,"children":646},{"className":645},[],[647],{"type":69,"value":648},"http://localhost:3000/callback",{"type":69,"value":150},{"type":60,"tag":79,"props":651,"children":653},{"className":652},[],[654],{"type":69,"value":655},"http://localhost:3000/callback/",{"type":69,"value":657}," are different URLs.",{"type":60,"tag":259,"props":659,"children":660},{},[661,673],{"type":60,"tag":65,"props":662,"children":663},{},[664,666,671],{"type":69,"value":665},"Skipping the ",{"type":60,"tag":79,"props":667,"children":669},{"className":668},[],[670],{"type":69,"value":156},{"type":69,"value":672}," check.",{"type":69,"value":674}," Without it, an attacker can complete a login flow in someone else's session (login CSRF).",{"type":60,"tag":259,"props":676,"children":677},{},[678,691,693,701,703,708,710,716],{"type":60,"tag":65,"props":679,"children":680},{},[681,683,689],{"type":69,"value":682},"Using ",{"type":60,"tag":79,"props":684,"children":686},{"className":685},[],[687],{"type":69,"value":688},"localhost",{"type":69,"value":690}," for a server-to-server callback.",{"type":69,"value":692}," A browser redirect to ",{"type":60,"tag":226,"props":694,"children":695},{"href":39},[696],{"type":60,"tag":79,"props":697,"children":699},{"className":698},[],[700],{"type":69,"value":38},{"type":69,"value":702}," works because the user's own browser follows it. A job-complete callback from a provider's servers can't reach your machine. Use a public URL that forwards to it, through a ",{"type":60,"tag":226,"props":704,"children":705},{"href":36},[706],{"type":69,"value":707},"tunnel",{"type":69,"value":709}," or ",{"type":60,"tag":226,"props":711,"children":713},{"href":712},"/glossary/webhook-forwarding/",[714],{"type":69,"value":715},"webhook forwarding",{"type":69,"value":347},{"type":60,"tag":259,"props":718,"children":719},{},[720,725],{"type":60,"tag":65,"props":721,"children":722},{},[723],{"type":69,"value":724},"Trusting the callback blindly.",{"type":69,"value":726}," For server-to-server callbacks, verify the signature or fetch the result from the provider's API before acting on it.",{"type":60,"tag":259,"props":728,"children":729},{},[730,735,737,743,745,751],{"type":60,"tag":65,"props":731,"children":732},{},[733],{"type":69,"value":734},"Slow handlers.",{"type":69,"value":736}," Server-to-server callbacks behave like webhooks: return ",{"type":60,"tag":79,"props":738,"children":740},{"className":739},[],[741],{"type":69,"value":742},"2xx",{"type":69,"value":744}," quickly or the provider may time out and retry. See ",{"type":60,"tag":226,"props":746,"children":748},{"href":747},"/glossary/webhook-retries/",[749],{"type":69,"value":750},"webhook retries",{"type":69,"value":347},{"type":60,"tag":89,"props":753,"children":755},{"id":754},"testing-callbacks",[756],{"type":69,"value":757},"Testing callbacks",{"type":60,"tag":61,"props":759,"children":760},{},[761,763,769,771,776,778,784],{"type":69,"value":762},"For browser-redirect callbacks during development, register a ",{"type":60,"tag":79,"props":764,"children":766},{"className":765},[],[767],{"type":69,"value":768},"http://localhost",{"type":69,"value":770}," redirect URI if the provider allows it. For server-to-server callbacks, point the provider at a ",{"type":60,"tag":226,"props":772,"children":773},{"href":22},[774],{"type":69,"value":775},"Webhook Bin",{"type":69,"value":777}," URL to see exactly what it sends, then run ",{"type":60,"tag":79,"props":779,"children":781},{"className":780},[],[782],{"type":69,"value":783},"relay forward --bucket my-app http://localhost:3000/callbacks",{"type":69,"value":785}," to deliver real callbacks to your local code through a permanent public URL.",{"type":60,"tag":787,"props":788,"children":789},"style",{},[790],{"type":69,"value":791},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":14,"depth":14,"links":793},[794,795,796,797,798],{"id":91,"depth":470,"text":94},{"id":239,"depth":470,"text":242},{"id":320,"depth":470,"text":323},{"id":621,"depth":470,"text":624},{"id":754,"depth":470,"text":757},"markdown","content:glossary:callback-url.md","content","glossary/callback-url.md","glossary/callback-url","md",{"loc":4},1793199804748]