# How do I add a redirect URI automatically when a customer adds a domain?

> Call your identity provider’s management API from the code that marks a customer’s domain as verified and active, and add that domain’s callback URL there. Run the call from a job queue so it retries, make it safe to repeat, and remove the URL when the customer disconnects the domain. With loginos you register the host with one POST, or let loginos ask your server.

A customer connects `app.customer.com` in your dashboard. Your app checks their DNS, gets a certificate, and the site loads. Then sign-in fails with a redirect URI error, because your identity provider only sends people back to callback URLs it knows. Someone on your team adds the URL by hand, and next week it happens again.

The fix is to make the callback URL part of the domain’s lifecycle in code. The step that makes a domain live registers it with your identity provider, and the step that retires it removes it.

## When should your app add the redirect URI?

Add the redirect URI when the customer’s domain becomes active, and not before. A custom domain usually moves through a few states in your database:

| State | What has happened | Redirect URI |
| --- | --- | --- |
| Pending | The customer typed the domain | Not registered |
| Verified | DNS shows the customer controls it | Not yet |
| Active | Your certificate is live and you serve the site | Add it |
| Removed | The customer disconnected it or left, or DNS stopped pointing at you | Remove it |

Registering a pending domain would let anyone put any name on your allow list by typing it into a form. Registering only active domains means every URL on the list belongs to a customer you checked.

Put both calls next to the code that changes the state, or in handlers for events like `domain.activated` and `domain.removed`. Then no path through your app can activate a domain without registering it.

## How do you make the API call reliable?

A call to your identity provider’s API can fail like any network request, and a missed call leaves a customer whose users can’t sign in. Four habits prevent that.

**Make it idempotent.** An idempotent operation has the same result however often you repeat it. Adding a URL that is already on the list should change nothing, and removing one that is already gone should count as done. If your provider’s API isn’t built that way, read the current list first and skip the call when there is nothing to do.

**Retry from a queue.** Save the state change and a “sync this domain” job in the same database transaction, and let a worker make the call. When the call fails, the job runs again later with backoff, and waits at least the `Retry-After` time when a `429` answer has one. Network errors, rate limits and `5xx` errors usually pass on their own. A `400` or `401` won’t, so after a few attempts the job should alert a person.

**Serialize changes to one list.** Some APIs take the whole list of redirect URIs in one field. Two workers that each read the list, add a URL and write it back can erase each other’s change. Run those updates one at a time, for example from a queue with a single worker.

**Reconcile on a schedule.** Once a day, compare your active domains with the provider’s list, then add what is missing and remove what is left over. That catches anything a bug or an outage let through.

## How do other identity providers handle new redirect URIs?

Auth0, Microsoft Entra ID and Amazon Cognito all keep redirect URIs as a list on the application, and your server can change it through a management API. They differ in how long that list may be.

| Provider | Where the list lives | How you change it | Limit |
| --- | --- | --- | --- |
| Auth0 | The application’s `callbacks` | `PATCH /api/v2/clients/{id}` in the Management API | 100 callback URLs per application |
| Microsoft Entra ID | The application’s `web.redirectUris` | `PATCH /applications/{id}` in Microsoft Graph | 256 redirect URIs, or 100 if the app also signs in personal Microsoft accounts |
| Amazon Cognito | The app client’s `CallbackURLs` | `UpdateUserPoolClient` | 100 per app client |

Auth0 says its limit generally can’t be raised, and Microsoft says its maximum can’t be raised for security reasons. Cognito’s update call resets every setting you leave out to its default, so read the current settings with `DescribeUserPoolClient` and send them back with your change.

Caps like these mean that a product with a few hundred custom domains has to spread customers over several applications, or put a central login domain in front of them. [Authentication on customers’ custom domains](https://loginos.io/answers/saas-custom-domain-authentication.md) compares those designs, and [wildcard redirect URIs](https://loginos.io/answers/wildcard-redirect-uri-oauth.md) explains why a pattern is no substitute.

## How loginos does it

loginos registers hosts, not URLs. The callback path is the same on every domain, so `app.customer.com` is all it needs, and a platform has no limit on domains.

### Register domains from your server

`POST /api/v1/domains` adds one host, `{"host": …}`, or up to 1,000, `{"hosts": [...]}`. Hosts that are already there are fine, so your queue can repeat the call safely. `DELETE /api/v1/domains/{host}` removes one, and sign-ins from that domain stop at once. The Management API allows 600 requests a minute per platform and answers `429` with `Retry-After` beyond that.

```js title="Node.js"
const LOGINOS = "https://loginos.io";
const auth = "Basic " + Buffer.from(`${process.env.LOGINOS_APP_KEY}:${process.env.LOGINOS_APP_SECRET}`).toString("base64");

// Your job queue runs these and retries with backoff when they throw.
export async function addDomain(host) {
  const res = await fetch(`${LOGINOS}/api/v1/domains`, {
    method: "POST",
    headers: { authorization: auth, "content-type": "application/json" },
    body: JSON.stringify({ host }),
  });
  if (!res.ok) throw new Error(`loginos answered ${res.status}`); // a host already there is fine
}

export async function removeDomain(host) {
  const res = await fetch(`${LOGINOS}/api/v1/domains/${encodeURIComponent(host)}`, {
    method: "DELETE",
    headers: { authorization: auth },
  });
  if (!res.ok && res.status !== 404) throw new Error(`loginos answered ${res.status}`); // 404: already gone
}
```

```python title="Python"
import os
import requests

LOGINOS = "https://loginos.io"
AUTH = (os.environ["LOGINOS_APP_KEY"], os.environ["LOGINOS_APP_SECRET"])

# Your job queue runs these and retries with backoff when they raise.
def add_domain(host: str) -> None:
    r = requests.post(f"{LOGINOS}/api/v1/domains", json={"host": host}, auth=AUTH, timeout=10)
    r.raise_for_status()  # a host already there is fine

def remove_domain(host: str) -> None:
    r = requests.delete(f"{LOGINOS}/api/v1/domains/{host}", auth=AUTH, timeout=10)
    if r.status_code != 404:  # 404: already gone
        r.raise_for_status()
```

To backfill the domains you already have, send them as `{"hosts": [...]}` in batches of 1,000. The answer lists what was `added`, what was `existing` and what was `invalid`, so you can fix the invalid ones. See [Domains: from your server](https://loginos.io/docs/domains.md#from-your-server) and the [Management API](https://loginos.io/docs/api.md#domains).

### Or let loginos ask your server

Automatic domains skip the API calls. The first time a sign-in comes from a host that isn’t one of your domains, loginos asks an endpoint on your server, and adds the host only if your server says yes.

loginos posts `{"host": "app.customer.com", "client_id": "<your app key>"}`. Answer from your own records, within 3 seconds:

```ts title="Next.js"
// app/api/loginos/domain-check/route.ts
import { db } from "@/lib/db";

export async function POST(req: Request) {
  const { host } = await req.json();
  // Verified and active in your own records. Nothing else counts.
  const domain = await db.customDomain.findUnique({ where: { host } });
  return Response.json({ host, allow: domain?.status === "active" });
}
```

Only a `200` with `{"host": <the same host>, "allow": true}` adds the host, marked as automatic. Anything else stops the sign-in: `"allow": false`, another host, another status, or no answer in time.

The endpoint must never:

- **Say yes to everything.** Your app key is public, so a look-alike site can send people to loginos with it. Before the feature turns on, loginos asks about a `.invalid` host, and an endpoint that says yes stays off.
- **Decide from the domain’s name or DNS.** Anyone can register a name that looks like yours and point it at your servers today and elsewhere tomorrow. The request’s `Host` header proves nothing either.

Removal takes care of itself. loginos asks again when an automatic domain is used a day after the last yes, and a clear no removes it. If you only delete an automatic domain, the next sign-in there adds it again, so answer no for domains you retire. loginos sends at most 120 checks a minute per platform and never adds wildcards, IP addresses or `localhost`. See [automatic domains](https://loginos.io/docs/domains.md#automatic-domains).

## Frequently asked questions

### What if my provider limits how many redirect URIs I can add?

You have three options. Spread customers over several applications. Move them to subdomains of one parent domain, if your provider offers a placeholder or wildcard for those. Or put a central login domain in front and hand sessions on to each custom domain. Each adds work, so check the limit before you reach it.

### Do www and the bare domain need separate redirect URIs?

Yes. `www.customer.com` and `customer.com` are different hosts, and exact matching treats their callback URLs as different. Register both if the customer serves both, or redirect one to the other before sign-in starts.

### Can the browser add the redirect URI?

No. Management APIs need your client secret or an admin token, which must stay on your server. Make the call from your backend or a worker, triggered by the domain’s state change.

### How do I test the lifecycle before customers use it?

Use a separate test client with its own credentials, and run the whole lifecycle on a test domain: connect, verify, sign in, disconnect, then check that sign-in stops. In loginos, you can register `localhost:3001` as a domain while you develop.

---

Updated 2026-10-14. Web page: https://loginos.io/answers/add-redirect-uri-automatically
