API Authentication
How third-party apps identify themselves, how users connect their Trailforks account with OAuth 2, and how to make signed calls on their behalf.
Request basics
Every method lives at https://www.trailforks.com/api/1/{method}.
Send parameters in the query string for GET. For POST, send them as form fields
or as a JSON body with Content-Type: application/json.
Authentication parameters go in the same place as the method's other parameters.
Every response, success or failure, is wrapped in the same envelope. The HTTP status code is also set.
{
"error": 0, // 0 = success, 1 = failure
"message": "", // human-readable detail, mostly on errors
"data": { ... } // method payload; on errors may carry { "code": 89 }
}
Credentials
There are two kinds of credential. App credentials identify your application. User tokens let your app act as a specific Trailforks user.
| Credential | Where it comes from | Lifetime | Keep secret? |
|---|---|---|---|
app_id | Issued by Trailforks when your app is approved. It is also your OAuth client_id. | Permanent | No |
app_secret | Issued with the app_id | Permanent | Yes. Server-side only. |
token_public | Returned by the OAuth token exchange (also returned as access_token) | About 180 days | Treat as sensitive |
token_secret | Returned with token_public. Used to sign write requests. It is never sent over the wire after this. | Same as its token | Yes. Server-side only. |
refresh_token | Returned with token_public | Until used or revoked | Yes. Server-side only. |
To request API access, contact us. Tell us your app's name, its website, and the endpoints you need. Some apps are limited to a set of endpoints or to one region. Calls outside those limits are rejected.
Auth levels
Each endpoint requires one of the levels below, and the endpoint reference says which. Read endpoints generally need only app credentials. Endpoints that return a user's private data need a user token. Endpoints that create or change content need a signed user token.
| Level | Used for | Send these parameters |
|---|---|---|
| app | Public data: trails, regions, POIs, reports, and similar | app_id, app_secret |
| user | Reading the connected user's own data | app_id, token_public |
| user + signature | Creating or changing content as the user, for example POST /report |
app_id, token_public, timestamp, hash |
app_secret.
The app_id you send must match the app the token was issued to.
Some read endpoints, such as trail, poi and route, require app credentials and also accept
an optional user token to personalise the response. For those, send app_id, app_secret
and token_public together.
Signing write requests
Signed calls prove you hold the token_secret without sending it. Compute a fresh signature for each request:
timestamp = current Unix time in seconds e.g. 1727200000
hash = lowercase hex SHA-1 of (timestamp + token_secret)
// plain string concatenation, timestamp first, no separator
Then send app_id, token_public, timestamp and hash with the request,
next to the method's own parameters.
Node.js
import crypto from "node:crypto";
function signParams(appId, tokenPublic, tokenSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const hash = crypto.createHash("sha1").update(timestamp + tokenSecret).digest("hex");
return { app_id: appId, token_public: tokenPublic, timestamp, hash };
}
Python
import hashlib, time
def sign_params(app_id, token_public, token_secret):
timestamp = str(int(time.time()))
digest = hashlib.sha1((timestamp + token_secret).encode()).hexdigest()
return {"app_id": app_id, "token_public": token_public,
"timestamp": timestamp, "hash": digest}
PHP
$timestamp = (string) time();
$hash = sha1($timestamp . $tokenSecret);
OAuth 2 (authorization code flow)
OAuth is the only supported way for a third-party app to act as a Trailforks user. The user signs in on trailforks.com and approves your app. Your app never sees their password. At the end of the flow you hold a normal Trailforks user token. It works with every user and user + signature endpoint your app has access to.
1. Send the user to the authorize page
https://www.trailforks.com/api/1/oauth2/auth
?client_id=YOUR_APP_ID
&response_type=code
&redirect_uri=https%3A%2F%2Fyourapp.example%2Ftrailforks%2Fcallback
&state=RANDOM_OPAQUE_VALUE
| Parameter | Required | Notes |
|---|---|---|
client_id | Yes | Your app_id |
redirect_uri | Yes | An absolute, URL-encoded HTTPS URL on a domain you control |
response_type | Yes | Always code |
state | Strongly recommended | An unguessable value tied to the user's session in your app. It is returned to you unchanged. Use letters, digits, - and _ only. |
scope | No | Accepted but not used yet. Tokens are not currently limited by scope. |
A user who isn't signed in sees the Trailforks / Outside sign-in page first. Then they see a consent screen: “Authorize {Your App} to connect to Trailforks.”
2. Receive the code at your redirect URI
If the user approves, Trailforks redirects to your redirect_uri with a one-time code:
https://yourapp.example/trailforks/callback?code=3f9c0e1b7a…&state=RANDOM_OPAQUE_VALUE
If the user cancels, you receive ?error=access_denied without a state.
Always check that state matches the value you sent before you continue.
3. Exchange the code for a user token (server-side)
This call uses app auth, so it needs your app_secret. Make it from your backend,
never from a browser or mobile client.
curl -X POST https://www.trailforks.com/api/1/token \
-d app_id=YOUR_APP_ID \
-d app_secret=YOUR_APP_SECRET \
-d code=3f9c0e1b7a…
200 OK
{
"error": 0,
"message": "",
"data": {
"access_token": "a1b2c3d4e5f6…", // same value as token_public
"token_public": "a1b2c3d4e5f6…",
"token_secret": "Xk29fPq…", // used to sign write calls, never sent
"refresh_token": "9e8d7c6b5a…",
"user_id": "123456",
"username": "trailbuilder",
"expires_on": 1742752000 // Unix seconds, about 180 days out
}
}
An invalid, expired or already-used code returns 401. Send the user through step 1 again.
Store token_public, token_secret, refresh_token, user_id
and expires_on against the user in your system. Encrypt the two secrets at rest.
4. Call the API as the user
For user endpoints, send app_id and token_public.
For user + signature endpoints, also send timestamp and hash,
as described in Signing write requests. See the POST /report example for a full call.
Refreshing and revoking tokens
Refresh before expiry
Before expires_on, or after an error with code 89, trade your refresh token for a new token set.
The signature here uses your app_secret, not the user's token secret: hash = sha1(timestamp + app_secret).
uuid is required. Send a stable identifier for this connection, such as your own ID for the user.
curl -G https://www.trailforks.com/api/1/get_token \
--data-urlencode app_id=YOUR_APP_ID \
--data-urlencode timestamp=1727200000 \
--data-urlencode hash=SHA1_OF_TIMESTAMP_PLUS_APP_SECRET \
--data-urlencode uuid=yourapp-user-8812 \
--data-urlencode refresh_token=9e8d7c6b5a…
The response has the same shape as the code exchange: token_public, token_secret,
refresh_token, userid and expires_on.
Refresh tokens rotate. Replace all three stored values each time.
Code 99 means the refresh token is no longer valid, and the user must authorize again.
Disconnect (revoke)
When a user disconnects Trailforks in your app, revoke your access. This removes every token your app holds for that user.
curl -G https://www.trailforks.com/api/1/oauth2/revoke \
--data-urlencode client_id=YOUR_APP_ID \
--data-urlencode app_id=YOUR_APP_ID \
--data-urlencode token_public=a1b2c3d4e5f6…
The response body is true if the revoke succeeded and false if it didn't.
A user can also sign out of your app from the Trailforks side. When that happens, your next call fails with 401.
Example: submitting a trail report with an OAuth token
POST /report requires
user + signature. Sign the request with the OAuth token_secret
and the report is filed as the connected user.
TS=$(date +%s)
HASH=$(printf '%s%s' "$TS" "$TOKEN_SECRET" | sha1sum | cut -d' ' -f1)
curl -X POST https://www.trailforks.com/api/1/report \
-d app_id=YOUR_APP_ID \
-d token_public="$TOKEN_PUBLIC" \
-d timestamp="$TS" \
-d hash="$HASH" \
-d userid=123456 \
-d action=add \
-d trailid=4321 \
-d status=1 \
-d condition=2 \
--data-urlencode "description=Dry and fast, one tree down near the bottom" \
-d reportuid=6f1c2a90-yourapp-0001
useridis required and must be the connected user'suser_idfrom the token response.- Send
trailidfor a trail report, orpoiidfor a point-of-interest report. reportuidis optional but recommended. It is an idempotency key: if you retry with the same value, no duplicate report is created.- The user's account must be verified. If it isn't, the call fails with code
90.
The endpoint reference lists the valid status and
condition values and the other optional fields.
Authentication errors
Auth failures return HTTP 401 with "error": 1.
Many also include a machine-readable data.code:
| HTTP | data.code | Meaning | What to do |
|---|---|---|---|
| 401 | 215 | Unknown token, or the token wasn't issued to this app_id | Check you are sending the right pair. If it keeps failing, re-authorize. |
| 401 | 89 | Token has expired | Refresh it |
| 401 | 99 | Token or refresh token has been invalidated (revoked, account changed) | Send the user through OAuth again |
| 401 | 32 | Invalid hash | Check the concatenation order, the secret, and that timestamp is in seconds |
| 401 | 90 | User account isn't verified | Ask the user to verify their email |
| 401 | — | Invalid app_id / app_secret combination | Check your app credentials |
| 405 | — | Your app isn't enabled for this endpoint | Contact us to extend your access |
| 503 | — | Your app has been disabled | Contact us |
| 400 | — | A required parameter is missing | The message names the parameter |
Security checklist
- Keep
app_secret,token_secretandrefresh_tokenon your server. Never ship them in browser JavaScript or a mobile app binary. - Make the code exchange and every signed call from your backend.
- Generate a new
statefor each authorization and verify it on the callback. - Use HTTPS for your
redirect_uri. Don't forward thecodeanywhere else. - Compute a new
timestampandhashfor every write request. Don't reuse them. - Prefer POST bodies to query strings for anything secret, so it stays out of server and proxy logs.
- Revoke tokens when a user disconnects, and delete what you stored.