Authentication and Token Lifecycle
The public token journey is:
API key
-> POST /api/v1/auth/token
-> access token + refresh token
-> Bearer requests
-> POST /api/v1/auth/token/refresh
-> rotated token pair
-> POST /api/v1/auth/token/revoke
An API key is not a Bearer token. Keep API keys, access tokens, and refresh tokens out of URLs, browser bundles, source control, and logs.
Exchange an API key for a token pair
POST/api/v1/auth/token
Open token exchange in the API Reference.
This anonymous operation accepts the API key in its JSON request body. Use the configured base URL and synthetic environment-variable values in examples:
- cURL
- JavaScript
- Python
- Java
curl -X POST "${MAILHUB_API_BASE_URL}/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d '{"apiKey":"'"${MAILHUB_API_KEY}"'"}'
const response = await fetch(`${process.env.MAILHUB_API_BASE_URL}/api/v1/auth/token`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({apiKey: process.env.MAILHUB_API_KEY}),
});
const {data: session} = await response.json();
// Retain session.accessToken, session.refreshToken, and both expiry fields.
import os
import requests
response = requests.post(
f"{os.environ['MAILHUB_API_BASE_URL']}/api/v1/auth/token",
json={"apiKey": os.environ["MAILHUB_API_KEY"]},
)
session = response.json()["data"]
# Retain session["accessToken"], session["refreshToken"], and both expiry fields.
// Java 11+ java.net.http — no MailHub package to install and no third-party client.
var request = HttpRequest.newBuilder()
.uri(URI.create(System.getenv("MAILHUB_API_BASE_URL") + "/api/v1/auth/token"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"apiKey\":\"" + System.getenv("MAILHUB_API_KEY") + "\"}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() with your own JSON library, then retain data.accessToken,
// data.refreshToken, and both expiry fields.
A successful response uses the public envelope and returns the values to retain for the session:
{
"success": true,
"data": {
"accountId": "<account-id>",
"apiKeyId": "<api-key-id>",
"accessToken": "<access-token>",
"refreshToken": "<refresh-token>",
"accessTokenExpiresAtUtc": "<utc-timestamp>",
"refreshTokenExpiresAtUtc": "<utc-timestamp>"
},
"errors": null
}
Retain the access token, refresh token, and their response expiry fields. The response fields are the source of truth; this portal does not publish token lifetime values.
Use the access token
Send the access token on protected public operations:
Authorization: Bearer <access-token>
Refresh once, then re-authenticate
POST/api/v1/auth/token/refresh
Open token refresh in the API Reference.
Send the retained refresh token in the JSON request body. Like token exchange,
this operation is anonymous — it carries no Authorization header.
- cURL
- JavaScript
- Python
- Java
curl -X POST "${MAILHUB_API_BASE_URL}/api/v1/auth/token/refresh" \
-H "Content-Type: application/json" \
-d '{"refreshToken":"'"${MAILHUB_REFRESH_TOKEN}"'"}'
const response = await fetch(`${process.env.MAILHUB_API_BASE_URL}/api/v1/auth/token/refresh`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({refreshToken: process.env.MAILHUB_REFRESH_TOKEN}),
});
const {data: session} = await response.json();
// Replace BOTH stored tokens before the next request.
import os
import requests
response = requests.post(
f"{os.environ['MAILHUB_API_BASE_URL']}/api/v1/auth/token/refresh",
json={"refreshToken": os.environ["MAILHUB_REFRESH_TOKEN"]},
)
session = response.json()["data"]
# Replace BOTH stored tokens before the next request.
var request = HttpRequest.newBuilder()
.uri(URI.create(System.getenv("MAILHUB_API_BASE_URL") + "/api/v1/auth/token/refresh"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"refreshToken\":\"" + System.getenv("MAILHUB_REFRESH_TOKEN") + "\"}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Replace BOTH stored tokens with data.accessToken and data.refreshToken.
A successful refresh returns the same fields as token exchange — a new access token and a new refresh token:
{
"success": true,
"data": {
"accountId": "<account-id>",
"apiKeyId": "<api-key-id>",
"accessToken": "<new-access-token>",
"refreshToken": "<new-refresh-token>",
"accessTokenExpiresAtUtc": "<utc-timestamp>",
"refreshTokenExpiresAtUtc": "<utc-timestamp>"
},
"errors": null
}
After a successful refresh, replace both stored tokens with the returned access token and refresh token before making another request. For a failed protected request, make at most one controlled refresh attempt. If refresh fails, re-authenticate with the API key. Do not create an infinite refresh or retry loop.
A failed refresh uses the standard error envelope — 400 for a request that
could not be read, 401 for a refresh token that is missing, malformed,
expired, or already revoked, and 422 when the supplied value does not pass
validation. See Authentication Errors.
Revoke when access should end
POST/api/v1/auth/token/revoke
Open token revocation in the API Reference.
Revocation takes the same anonymous, refresh-token body as refresh does.
- cURL
- JavaScript
- Python
- Java
curl -X POST "${MAILHUB_API_BASE_URL}/api/v1/auth/token/revoke" \
-H "Content-Type: application/json" \
-d '{"refreshToken":"'"${MAILHUB_REFRESH_TOKEN}"'"}'
const response = await fetch(`${process.env.MAILHUB_API_BASE_URL}/api/v1/auth/token/revoke`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({refreshToken: process.env.MAILHUB_REFRESH_TOKEN}),
});
const {data} = await response.json();
// data.isSuccess is the documented operation result.
import os
import requests
response = requests.post(
f"{os.environ['MAILHUB_API_BASE_URL']}/api/v1/auth/token/revoke",
json={"refreshToken": os.environ["MAILHUB_REFRESH_TOKEN"]},
)
result = response.json()["data"]
# result["isSuccess"] is the documented operation result.
var request = HttpRequest.newBuilder()
.uri(URI.create(System.getenv("MAILHUB_API_BASE_URL") + "/api/v1/auth/token/revoke"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"refreshToken\":\"" + System.getenv("MAILHUB_REFRESH_TOKEN") + "\"}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Read data.isSuccess from response.body() with your own JSON library.
A successful revocation returns the bounded operation result and nothing else:
{
"success": true,
"data": {
"isSuccess": true
},
"errors": null
}
Revoke the refresh session when access should end. Check the public response before discarding your locally retained credentials.
Sub-account scoping
The Sub-account must belong to the authenticated Account. Use the same optional scope consistently for one workflow, but do not assume it has one uniform effect across every operation:
- Token exchange validates the supplied Sub-account but does not persist it in either token.
- List Orders filters the list, and Order creation tags the new Order.
- Rates and Label purchase evaluate the request against the selected Sub-account's carrier availability, which can narrow the carriers on offer. It does not change pricing, and it does not restrict record access.
- Get/update an Order, download/cancel a Label, and refresh/revoke have no effect from a well-formed header.
A malformed header can return 400. Token exchange can return 404 when the
Sub-account is unavailable to the Account and 409 when it is inactive. See
Work with Sub-accounts for the complete
operation-by-operation matrix and the API Reference for each
operation's current header description.
Next step
Continue to Make Your First Request for the safe exchange-then-list flow.