uSend Docs
API endpoints

OAuth

Let your app act on a usend organization with OAuth 2.0.

Use OAuth when an app you build needs to act on other people's usend organizations without asking them to paste an API key. See Building an OAuth client for a walkthrough.

usend supports public clients only (no client secret), the authorization code flow with mandatory PKCE (S256), and refresh tokens. Discovery metadata is published at https://api.usend.et/.well-known/oauth-authorization-server.

All token, register and revoke endpoints accept application/x-www-form-urlencoded or JSON, allow requests from any origin (no cookies are involved), and answer errors as { "error": "...", "error_description": "..." }.

Scopes

ScopeAllows
sending_accessSend emails (POST /emails, POST /emails/batch).
full_accessEverything an API key with full access can do, except creating API keys. Only organization owners and admins can grant it.

A token acts for exactly one organization: the one the user chose on the consent screen.

Register a client

POST /oauth/register

NameTypeRequiredDescription
client_namestringYesShown to users on the consent screen, up to 100 characters.
redirect_urisstring[]Yes1 to 10 URIs. Must be https, or http on localhost, 127.0.0.1 or [::1]. No fragments. Matched exactly.
token_endpoint_auth_methodstringNoOnly none.
cURL
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"] }'
Response (201)
{
  "client_id": "usend_Qm3x...",
  "client_id_issued_at": 1760000000,
  "client_name": "Acme Mailer",
  "redirect_uris": ["https://acme.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "full_access sending_access"
}

Authorize

Send the user's browser to https://app.usend.et/oauth/authorize with these query parameters.

NameRequiredDescription
response_typeYescode.
client_idYesFrom registration.
redirect_uriYesOne of the registered URIs, exactly.
scopeYesSpace-separated scopes.
code_challengeYesBase64url SHA-256 of your code_verifier.
code_challenge_methodYesS256.
stateRecommendedReturned unchanged. Check it.

The user signs in, picks the organization and approves. usend redirects to your redirect_uri with code, state and iss, or with error=access_denied. A request with an unknown client or a redirect URI that does not match is never redirected: the user sees an error instead.

Exchange the code

POST /oauth/token

NameDescription
grant_typeauthorization_code.
client_idYour client ID.
codeThe authorization code. Valid for 60 seconds, single use.
code_verifierThe PKCE verifier (43 to 128 characters).
redirect_uriThe same URI as in the authorization request.
Response
{
  "access_token": "usa_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "usr_...",
  "scope": "full_access"
}

Use the access token like an API key: Authorization: Bearer usa_....

Refresh a token

POST /oauth/token with grant_type=refresh_token, client_id and refresh_token. The response has the same shape and a new refresh token. Refresh tokens are single use: store the new one and discard the old one. If a refresh token is used twice, usend revokes the whole grant and your app must ask the user to authorize again.

Revoke a token

POST /oauth/revoke with token and optionally client_id. Always answers 200. Revoking a refresh token ends the grant; revoking an access token removes that token only.

Connected apps

Users see and revoke the apps they connected in Settings under Connected apps, or with the session-only endpoints GET /oauth/grants and DELETE /oauth/grants/{id}.

Errors

errorMeaning
invalid_requestA parameter is missing or malformed.
invalid_clientUnknown client_id.
invalid_grantThe code or refresh token is wrong, expired, used, or its grant was revoked; or the verifier or redirect URI does not match.
unsupported_grant_typeUse authorization_code or refresh_token.
invalid_redirect_uri / invalid_client_metadataRegistration was refused.
rate_limit_exceededToo many requests. Retry after the Retry-After header.