uSend Docs
Guides

Building an OAuth client

Connect your app to a customer's usend organization with the authorization code flow and PKCE.

This guide builds the full flow with plain HTTP and Node.js. The same steps apply in any language. See the OAuth reference for every parameter.

1. Discover the endpoints

Shell
curl https://api.usend.et/.well-known/oauth-authorization-server

The response lists authorization_endpoint, token_endpoint, registration_endpoint and revocation_endpoint.

2. Register your app once

Shell
curl -X POST https://api.usend.et/oauth/register \
  -H "Content-Type: application/json" \
  -d '{ "client_name": "Acme Mailer", "redirect_uris": ["https://acme.example/callback"] }'

Keep the client_id. There is no secret: your app is a public client, and PKCE protects the flow. Redirect URIs are matched exactly. For local development use http://localhost:3000/callback or http://127.0.0.1:3000/callback.

Create a random code_verifier and state for each attempt, and keep them in the user's session.

Node.js
import { createHash, randomBytes } from 'node:crypto';

const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
const state = randomBytes(16).toString('base64url');

const url = new URL('https://app.usend.et/oauth/authorize');
url.search = new URLSearchParams({
  response_type: 'code',
  client_id: CLIENT_ID,
  redirect_uri: 'https://acme.example/callback',
  scope: 'sending_access',
  code_challenge: challenge,
  code_challenge_method: 'S256',
  state,
}).toString();
// redirect the browser to url.toString()

Ask for the smallest scope you need. The user chooses which organization to connect, and your token only ever works for that one.

4. Handle the redirect

usend redirects to your redirect_uri with code and state. Check that state matches what you stored, then exchange the code within 60 seconds:

Node.js
const res = await fetch('https://api.usend.et/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    code,
    code_verifier: verifier,
    redirect_uri: 'https://acme.example/callback',
  }),
});
const { access_token, refresh_token, expires_in } = await res.json();

If the user clicked Deny, the redirect carries error=access_denied instead of a code.

5. Call the API

Shell
curl https://api.usend.et/emails \
  -H "Authorization: Bearer usa_..." \
  -H "Content-Type: application/json" \
  -d '{ "from": "Acme <[email protected]>", "to": ["[email protected]"], "subject": "Hi", "html": "<p>Hello</p>" }'

Access tokens last one hour and work like an API key for one organization.

6. Refresh

When a call returns 403 invalid_api_key or expires_in has passed, refresh:

Node.js
const res = await fetch('https://api.usend.et/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({ grant_type: 'refresh_token', client_id: CLIENT_ID, refresh_token }),
});

Every refresh returns a new refresh token and spends the old one. Save the new one before you use the new access token, and never run two refreshes with the same token in parallel: a reused refresh token revokes the grant, and the user has to connect again.

7. Disconnect

Call POST /oauth/revoke with the refresh token when the user disconnects your app. Users can also remove your app in the dashboard under Settings, Connected apps.

Security checklist

  • Always send and verify state.
  • Generate a new code_verifier per attempt.
  • Store tokens encrypted and never log them.
  • Handle invalid_grant by asking the user to authorize again.