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.

CredentialWhere it comes fromLifetimeKeep secret?
app_idIssued by Trailforks when your app is approved. It is also your OAuth client_id.PermanentNo
app_secretIssued with the app_idPermanentYes. Server-side only.
token_publicReturned by the OAuth token exchange (also returned as access_token)About 180 daysTreat as sensitive
token_secretReturned with token_public. Used to sign write requests. It is never sent over the wire after this.Same as its tokenYes. Server-side only.
refresh_tokenReturned with token_publicUntil used or revokedYes. 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.

LevelUsed forSend 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
Note: User-level endpoints identify your app by the token, so you don't send 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);
Headers: The four signing values are also accepted as HTTP headers with the same names. Many proxies and load balancers drop header names that contain underscores, so sending them as request parameters is more reliable.

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
ParameterRequiredNotes
client_idYesYour app_id
redirect_uriYesAn absolute, URL-encoded HTTPS URL on a domain you control
response_typeYesAlways code
stateStrongly recommendedAn unguessable value tied to the user's session in your app. It is returned to you unchanged. Use letters, digits, - and _ only.
scopeNoAccepted 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.

The code expires in 5 minutes and can be exchanged only once. Exchange it right away, from your server.

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

OAuth tokens work with POST /report. The OAuth exchange creates the same kind of user token as every other sign-in. 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
  • userid is required and must be the connected user's user_id from the token response.
  • Send trailid for a trail report, or poiid for a point-of-interest report.
  • reportuid is 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:

HTTPdata.codeMeaningWhat to do
401215Unknown token, or the token wasn't issued to this app_idCheck you are sending the right pair. If it keeps failing, re-authorize.
40189Token has expiredRefresh it
40199Token or refresh token has been invalidated (revoked, account changed)Send the user through OAuth again
40132Invalid hashCheck the concatenation order, the secret, and that timestamp is in seconds
40190User account isn't verifiedAsk the user to verify their email
401—Invalid app_id / app_secret combinationCheck your app credentials
405—Your app isn't enabled for this endpointContact us to extend your access
503—Your app has been disabledContact us
400—A required parameter is missingThe message names the parameter

Security checklist

  • Keep app_secret, token_secret and refresh_token on 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 state for each authorization and verify it on the callback.
  • Use HTTPS for your redirect_uri. Don't forward the code anywhere else.
  • Compute a new timestamp and hash for 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.