Guides / For developers

Sign in with OAuth by hand

Discover the server, register a client, send a person through the Connect page with PKCE and trade the code for tokens, one curl request at a time.

7 min read. Updated .

To get a Zorlya access token by hand, discover the server, register a client, send a person through the Connect page with a PKCE challenge, and trade the code at the token endpoint. This guide runs each step with curl, so you see what every client does for you.

You need curl, openssl, a browser and an approved Zorlya account. A job seeker account needs the Pro plan, because AI client access is part of Pro.

Two hosts are involved. https://mcp.zorlya.com is the MCP server. https://api.zorlya.com is the authorization server (the issuer) for every Zorlya resource, and it serves the Connect page and the token endpoint.

Only AI apps on Zorlya’s list can connect. The list holds Claude, ChatGPT, Claude Code and Cursor, each with the exact return addresses its vendor uses. Registration is refused for any other return address. A program of your own that is not on the list cannot get a client. The loopback address used below, http://127.0.0.1:8976/callback, is one the list allows for Claude Code (any port, the path /callback), so the registration works, and the Connect page shows the name Claude Code, because the name comes from the list and not from what you send. Use this only to learn the flow with your own account.

1. Discover the server

A client given only the MCP address finds everything else from two documents. Fetch both:

curl -s https://mcp.zorlya.com/.well-known/oauth-protected-resource
curl -s https://api.zorlya.com/.well-known/oauth-authorization-server

The first names the resource (https://mcp.zorlya.com) and its authorization server (https://api.zorlya.com). The second lists the endpoints:

Table
Field What it holds
authorization_endpoint Where you send the person: /oauth/authorize
token_endpoint Where you trade a code or a refresh token: /oauth/token
registration_endpoint Where a new client registers: /oauth/register
revocation_endpoint Where you end a grant: /oauth/revoke
scopes_supported zorlya:read, zorlya:write and zorlya:delete
authorization_response_iss_parameter_supported true: the redirect back to you carries iss=https://api.zorlya.com
code_challenge_methods_supported S256 only
token_endpoint_auth_methods_supported none: every client is public and PKCE is the proof

You can also reach the first document from a refusal. A POST to https://mcp.zorlya.com with no token answers 401 with a WWW-Authenticate header that carries resource_metadata= pointing at it, and a scope. The same document is also served at /.well-known/oauth-protected-resource/mcp, for clients that derive that path from a resource written with /mcp. api.zorlya.com serves neither, and mcp.zorlya.com does not serve the authorization server document.

2. Register a client

Registration follows the dynamic client registration standard (RFC 7591), and Zorlya limits it to the list of AI apps. It grants nothing by itself.

curl -s -X POST https://api.zorlya.com/oauth/register \
  -H 'content-type: application/json' \
  -d '{
    "client_name": "My first client",
    "redirect_uris": ["http://127.0.0.1:8976/callback"],
    "token_endpoint_auth_method": "none",
    "scope": "zorlya:read"
  }'

The answer is 201 and includes a client_id. Keep it. Rules to know:

  • Redirect addresses must all belong to one enabled entry on the list. Anything else, including a custom scheme like myapp://callback, plain http to a real host or a mix of two apps’ addresses, is refused with 400 invalid_redirect_uri.
  • The name you send is ignored. The client is stored under the list entry’s name and publisher.
  • Scope is what the client may ask for later. Register zorlya:read zorlya:write if you will use the write tools.
  • Client authentication is none. The other methods in the registration standard are accepted at registration, but every client is treated as public.

3. Make a PKCE pair

PKCE ties the code you receive to the client that asked for it. Make a random verifier, then its challenge:

VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')

The challenge is the SHA-256 of the verifier, base64url encoded with no padding. The server accepts only the S256 method, so plain is refused.

4. Send the person to the Connect page

Build the address and open it in a browser. The resource parameter (RFC 8707) names the server the token is for. Leave it out and you get the MCP server. The only other resource Zorlya defines, https://api.zorlya.com, is switched off and answers invalid_target.

CLIENT_ID=paste-your-client-id
echo "https://api.zorlya.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8976%2Fcallback&code_challenge=$CHALLENGE&code_challenge_method=S256&scope=zorlya%3Aread&resource=https%3A%2F%2Fmcp.zorlya.com&state=any-random-text"

The Connect page opens. It names the client and its publisher, lists the Zorlya accounts you can use (job seeker, hiring, support, staff) and offers the permission levels, with Read only selected. Choose an account and a level. The browser then goes to that account’s own sign-in page, which shows what you are approving. Sign in with your password or a passkey. Google and GitHub also work for Read only. If you chose a level that changes data, Zorlya asks for the password or a passkey even when you are already signed in. The request waits for 10 minutes, works once and is tied to the browser that started it, so use one browser and allow cookies. You also get an email about the connection.

The browser then goes to your redirect address with three values in the query: code, your state and iss. Nothing is listening on port 8976, so the browser shows a connection error. That is expected. Copy code from the address bar. Check that state is the text you sent, which is how a real client knows the answer belongs to its request, and that iss is https://api.zorlya.com, which is how it knows the answer came from the server it asked.

If the person cancels, the redirect carries error=access_denied instead of a code. A bad scope, a missing code_challenge or a resource Zorlya does not serve reaches you the same way, as error and error_description in the query of your redirect address, with your state and iss. An unknown client_id or a redirect_uri that does not match exactly is different: the server shows an error page on api.zorlya.com and does not redirect, because it cannot trust the address.

5. Trade the code for tokens

The code works once and lasts 60 seconds, so do this now:

curl -s -X POST https://api.zorlya.com/oauth/token \
  -d grant_type=authorization_code \
  -d "code=PASTE_THE_CODE" \
  -d "client_id=$CLIENT_ID" \
  --data-urlencode "redirect_uri=http://127.0.0.1:8976/callback" \
  --data-urlencode "code_verifier=$VERIFIER" \
  --data-urlencode "resource=https://mcp.zorlya.com"

The resource must be the one the code was issued for. The server keeps the access token’s audience fixed to it.

The answer:

{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "zorlya:read",
  "refresh_token": "…"
}

The access token lasts one hour and the refresh token 30 days. Send the access token on every call to https://mcp.zorlya.com as Authorization: Bearer …. A token for another audience is refused with 401. Make your first calls does that next.

When a step fails

Table
You see Cause
400 invalid_redirect_uri at registration The address does not belong to an AI app on Zorlya’s list
An error page at authorize, no redirect The client_id is unknown or the redirect_uri differs from the registered one
error=invalid_target on your redirect address The resource is not https://mcp.zorlya.com
“That request could not be verified” on the Connect page Cookies are blocked, or the page was opened in another browser than the one that started it. Start again
error=invalid_request on your redirect address A missing code_challenge, or a code_challenge_method other than S256. The error comes back in the query, not as a page
invalid_grant, “incorrect code_verifier” The verifier is not the one that made the challenge
invalid_grant, “has already been used” The code was traded twice. The second attempt also ends the tokens the first one issued
invalid_grant, “has expired” More than 60 seconds passed between the redirect and the exchange
invalid_request, “redirect_uri did not match” The exchange sent a different redirect_uri from the authorize step

What we checked

The first version of this guide was run against a local copy of the server on 2026-10-17. On 2026-10-18 it was rewritten for the new hosts, the list of AI apps and the Connect page from the server’s source and its tests. The commands were not run again after the rewrite and none was run against the production server.