Forstify External API
Version: 1.1 ·
Base URL: https://app.forstify.de/external/api/v1 ·
Swagger UI: https://app.forstify.de/api/v2/forstify-api
See the Changelog for what's new in 1.1.
Changelog
All changes below are additive/optional — existing 1.0 integrations keep working unchanged.
1.1
GET /measurements,/measurements/overview,/woodlistsand/woodlists/overviewnow return an optionaldeletedIdsarray — ids deleted sinceupdatedAfter— wheneverupdatedAfteris supplied. It's omitted entirely on full syncs (noupdatedAfter), so existing clients that don't look for it are unaffected. It reflects the whole tombstone set for the givenupdatedAfterregardless oflimit/offset, so it's only attached to the first page (offset=0) — later pages omit it. See GET /measurements and GET /woodlists for the response shape.- Fixed a caching bug: deleting a record didn't used to change the collection's
ETag, so a client polling withIf-None-Matchcould keep getting304 Not Modifiedand never learn about the deletion. Deletions now always change theETag. /measurements/overviewand/woodlists/overviewnow document theupdatedAfterparameter, which they already accepted.deletedIdsis capped at 1000 ids per request, on all four endpoints. If more qualify, the response omitsdeletedIdsand returnsresyncRequired: trueinstead of silently truncating. Wood-list deletion tombstones are additionally only retained for 90 days —GET /woodlistsand/woodlists/overviewalso setresyncRequired: truewhen the givenupdatedAfteris older than that window (measurement tombstones never expire, so/measurementscan only hit the 1000-id cap). Either way, dropupdatedAfterand perform a full sync when you seeresyncRequired.GET /measurements/overviewnow includes an optionalvarietyfield (WoodVariety | null) on each logpile summary. See LogpileShort.- API tokens can be team-scoped, in addition to personal tokens. A team token returns the team's data on every endpoint; a personal token returns only entries that are not in a team. A team token whose owner no longer owns the team is rejected with
403and the new error codeteam.access_revoked. Delta sync (updatedAfter) is not supported with team tokens, see Personal and team tokens.
1.0
Initial release.
Authentication
Every request must include your API key in the X-API-KEY header. Keys are managed in the Forstify app under My Account → Connected Apps (a Business-plan feature).
X-API-KEY: {prefix}.{token}
Requests with a missing or invalid key return 401 Unauthorized. Endpoints require a plan with a monthly API allowance greater than zero (see Rate & monthly limits); accounts without one get 402 Payment Required.
FORSTIFY_API_KEY). Anyone with your token can consume your monthly quota and access your data.
Personal and team tokens
A token is either personal or team-scoped. The scope decides which data every endpoint (including the image endpoints) returns:
| Token | Returns |
|---|---|
| Personal | Measurements and wood lists you created that are not in a team |
| Team | All measurements and wood lists of that team, regardless of who created them |
- The two scopes don't overlap: an entry is in the personal token's results until it is moved into a team, and from then on only in that team's (in full syncs, see the delta sync limits below).
- Only the owner of a team can create a team token.
- On every request the API checks that the token's owner still owns the team. If not (ownership transferred, or the owner left), the request fails with
403and the codeteam.access_revoked. - Delta sync with a team token is not supported. With
updatedAftera team token always getsresyncRequired: trueand nodeletedIds. Sync in full and useETag/If-None-Matchto skip unchanged responses. - Delta sync with a personal token is not told when an entry moves into a team: the entry is no longer returned, but it is not in
deletedIdseither, so a client that keeps a local copy keeps a stale one. After entries were moved into a team, do a full sync.
Rate & monthly limits
Monthly request quota
Each token's plan grants a monthly request allowance, shared across all API tokens of that account. The quota resets on the 1st of each month. Once exhausted, requests return 429 Too Many Requests with code quota.exceeded and a Retry-After header (seconds until the next monthly reset).
GET /version is free (0). GET /images/:id and GET /images/:id/:width/:height cost 3. Only responses that complete successfully (2xx) consume quota; 304 and error responses never do.
Rate limits per endpoint
| Endpoint | Rate limit | Quota cost |
|---|---|---|
GET /measurements | 20 req / 60 s | 1 |
GET /woodlists | 20 req / 60 s | 1 |
GET /measurements/overview | 30 req / 60 s | 1 |
GET /woodlists/overview | 30 req / 60 s | 1 |
GET /measurements/search | 60 req / 60 s (default) | 1 |
GET /woodlists/search | 60 req / 60 s (default) | 1 |
GET /measurements/:id | 60 req / 60 s (default) | 1 |
GET /woodlists/:id | 60 req / 60 s (default) | 1 |
GET /images/:id | 60 req / 60 s | 3 |
GET /images/:id/:width/:height | 60 req / 60 s | 3 |
GET /version | no rate limit | 0 |
Rate limits are tracked per API token (by its prefix), not per IP. Exceeding one returns 429 Too Many Requests (same envelope as all other errors, see below). A rate-limit 429 currently carries the generic code error; only the monthly quota error carries quota.exceeded. Tell the two apart by code, and wait for the time in Retry-After before retrying either.
Rate & quota response headers
Every successfully authenticated response carries headers so clients don't have to discover limits by hitting 429. A missing or invalid API key, an IP not on the token's allowlist, or a monthly-quota-exhausted 429 are rejected before these are computed, so those responses don't carry them.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per window for this endpoint |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Seconds until the window resets |
X-Quota-Remaining | Monthly quota units left. Reflects this request's own cost once it succeeds (2xx); on 304/4xx/5xx nothing is billed, so it reflects the value as of request start instead |
Retry-After | Present on 429 only — seconds until the rate limit window or monthly quota resets |
Caching (ETag / If-None-Match)
All list and search endpoints include an ETag response header. Send it as If-None-Match on subsequent requests; the server returns 304 Not Modified with an empty body if nothing changed. Deleting a record always changes the ETag, so a client that only re-fetches on 200 will still be told to re-fetch after a deletion (see 1.1).
# First request
GET /external/api/v1/measurements
→ 200 OK
→ ETag: "a1b2c3"
# Subsequent request
GET /external/api/v1/measurements
If-None-Match: "a1b2c3"
→ 304 Not Modified (empty body)
Pagination
All list endpoints return a paginated envelope:
{
"data": [...],
"pagination": {
"count": 20,
"limit": 20,
"offset": 0,
"total": 142
}
}
| Field | Type | Description |
|---|---|---|
data | array | Items on this page |
pagination.count | integer | Number of items in data (≤ limit) |
pagination.limit | integer | Echoes the requested limit |
pagination.offset | integer | Echoes the requested offset |
pagination.total | integer | Total items matching the query, across all pages |
Query parameters
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
limit | integer | 20 | 100 | Items per page |
offset | integer | 0 | 100000 | Number of items to skip |
ID format
All resource IDs are GUIDs generated by MSSQL's NEWSEQUENTIALID(). This function produces GUIDs with an irregular version nibble, for example BAEB1143-2BFD-F011-8332-6045BD9D30A4, where position 13 can be any hex digit instead of the range from 1 through 8 required by RFC 4122.
Most runtimes are unaffected. Only Node.js UUID validators enforce the version nibble by default:
| Library | Default result | Fix |
|---|---|---|
validator.js isUUID(id) | rejects | Use isUUID(id, 'loose'); this disables the version/variant check |
uuid npm validate(id) | rejects | No built in option; use isUUID(id, 'loose') from validator.js or the regex below |
Java UUID.fromString() | accepts | n/a |
Python uuid.UUID(str) | accepts | n/a |
Go uuid.Parse() | accepts | n/a |
As a last resort, use this lenient regex, which accepts any GUID made of five hex digit groups (lengths 8, 4, 4, 4 and 12) regardless of version nibble:
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
Error responses
Every error returned by an endpoint of this API, regardless of cause, uses one envelope:
{
"statusCode": 429,
"code": "quota.exceeded",
"message": "..." // present on validation, not-found and rate-limit style errors; omitted for auth/plan/IP/team/quota errors
}
| code | Status | Meaning |
|---|---|---|
auth.invalid | 401 | API key missing, malformed, or unknown |
plan.required | 402 | Account's plan has no monthly API allowance |
ip.blocked | 403 | Token has an IP allowlist and the request IP isn't in it |
quota.exceeded | 429 | Monthly quota exhausted; check the Retry-After header |
validation.failed | 400 | Query or path parameter failed validation; see message for detail |
not_found | 404 | The requested record doesn't exist or isn't visible to this account |
team.access_revoked | 403 | Team-scoped token whose owner no longer owns the team; the team's current owner has to create a new team token |
error | any | Generic code for errors without a specific one, for example a rate-limit 429; see message and Retry-After |
Requests that never reach an endpoint, an unknown path or a method other than GET (this API is read-only), return 404 with the framework's default body, outside this envelope: { "statusCode": 404, "error": "Not Found", "message": "Cannot POST /external/api/v1/measurements/..." }. There is no code on these.
Example validation error, with detail in message:
{
"statusCode": 400,
"code": "validation.failed",
"message": "at least one search parameter required"
}
Endpoints
Returns current backend and API version. No rate limit and free of quota cost (0); safe to poll.
Response 200
{
"backendVersion": "4.8.0",
"apiVersion": "1.1"
}
Example
// Node.js 22+ (built-in fetch) · npm install express @types/express
import express from 'express';
const API_BASE = 'https://app.forstify.de/external/api/v1';
const API_KEY = process.env.FORSTIFY_API_KEY!;
const app = express();
app.get('/forstify-version', async (_req, res) => {
const response = await fetch(`${API_BASE}/version`, {
headers: { 'X-API-KEY': API_KEY },
});
if (!response.ok) {
return res.status(response.status).json(await response.json());
}
res.json(await response.json());
});
// Spring Boot 3.2+ · spring-boot-starter-web
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.Map;
@Service
public class ForstifyClient {
private final RestClient restClient;
public ForstifyClient(@Value("${forstify.api-key}") String apiKey) {
this.restClient = RestClient.builder()
.baseUrl("https://app.forstify.de/external/api/v1")
.defaultHeader("X-API-KEY", apiKey)
.build();
}
public Map<String, String> getVersion() {
return restClient.get()
.uri("/version")
.retrieve()
.body(new org.springframework.core.ParameterizedTypeReference<>() {});
}
}
# pip install requests
import os
import requests
class ForstifyClient:
BASE_URL = "https://app.forstify.de/external/api/v1"
def __init__(self):
self._session = requests.Session()
self._session.headers["X-API-KEY"] = os.environ["FORSTIFY_API_KEY"]
def get_version(self) -> dict:
resp = self._session.get(f"{self.BASE_URL}/version")
resp.raise_for_status()
return resp.json()
# Usage
client = ForstifyClient()
version = client.get_version()
print(version["backendVersion"])
// stdlib only
package forstify
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type Client struct {
baseURL string
apiKey string
http *http.Client
}
func NewClient() *Client {
return &Client{
baseURL: "https://app.forstify.de/external/api/v1",
apiKey: os.Getenv("FORSTIFY_API_KEY"),
http: &http.Client{Timeout: 30 * time.Second},
}
}
type VersionResponse struct {
BackendVersion string `json:"backendVersion"`
APIVersion string `json:"apiVersion"`
}
func (c *Client) GetVersion() (*VersionResponse, error) {
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/version", nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result VersionResponse
json.NewDecoder(resp.Body).Decode(&result)
return &result, nil
}
Returns all logpile measurements belonging to the authenticated user, paginated. Includes full detail: volumes, single logs, metadata.
Query parameters
| Parameter | Type | Default | Options | Description |
|---|---|---|---|---|
limit | integer | 20 | 1 through 100 | Items per page |
offset | integer | 0 | 0 through 100000 | Pagination offset |
sortBy | string | createdAt | createdAt updatedAt number species | Sort field |
sortOrder | string | ASC | ASC DESC | Sort direction |
updatedAfter | ISO 8601 datetime | n/a | n/a | Only records updated after this timestamp, for delta sync |
Response 200
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-06-01T08:15:00.000Z",
"logpileNumber": "2024-001",
"species": "fi",
"assortment": "Stammholz",
"variety": "Lang",
"logCount": 48,
"logpileWidth": 4.5,
"latitude": 47.8123,
"longitude": 13.0456,
"notes": null,
"images": ["https://..."],
"volumes": [
{ "number": 1, "quality": "b", "logLength": 4.0, "volume": 12.5, "logCount": 48 }
],
"singleLogs": null,
"metadata": { "forestOwner": "Muster GmbH", "lotNumber": "42" }
}
],
"pagination": { "count": 1, "limit": 20, "offset": 0, "total": 84 },
"deletedIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
}
deletedIds (optional, added in 1.1): only present when updatedAfter is supplied, and only on the first page (offset=0) — ids of measurements deleted since that timestamp.
resyncRequired (optional, added in 1.1): present instead of deletedIds when the deletion set exceeds the 1000-id per-request cap, or whenever updatedAfter is supplied with a team token (see Personal and team tokens). Drop updatedAfter and perform a full sync when you see this.
Example
app.get('/measurements', async (req, res) => {
const params = new URLSearchParams({
limit: String(req.query.limit ?? 20),
offset: String(req.query.offset ?? 0),
sortBy: String(req.query.sortBy ?? 'createdAt'),
sortOrder: String(req.query.sortOrder ?? 'DESC'),
});
const response = await fetch(`${API_BASE}/measurements?${params}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 304) return res.status(304).end();
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
import org.springframework.core.ParameterizedTypeReference;
import java.util.List;
import java.util.Map;
public Map<String, Object> getMeasurements(
int limit, int offset, String sortBy, String sortOrder) {
return restClient.get()
.uri(u -> u.path("/measurements")
.queryParam("limit", limit)
.queryParam("offset", offset)
.queryParam("sortBy", sortBy)
.queryParam("sortOrder", sortOrder)
.build())
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
def get_measurements(
self,
limit: int = 20,
offset: int = 0,
sort_by: str = "createdAt",
sort_order: str = "DESC",
) -> dict:
resp = self._session.get(
f"{self.BASE_URL}/measurements",
params={"limit": limit, "offset": offset, "sortBy": sort_by, "sortOrder": sort_order},
)
resp.raise_for_status()
return resp.json()
// Inside Client - see GET /version for full struct setup
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
)
type Pagination struct {
Count int `json:"count"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
}
type PaginatedResult struct {
Data []map[string]any `json:"data"`
Pagination Pagination `json:"pagination"`
}
func (c *Client) GetMeasurements(limit, offset int, sortBy, sortOrder string) (*PaginatedResult, error) {
q := url.Values{}
q.Set("limit", strconv.Itoa(limit))
q.Set("offset", strconv.Itoa(offset))
q.Set("sortBy", sortBy)
q.Set("sortOrder", sortOrder)
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/measurements?"+q.Encode(), nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result PaginatedResult
json.NewDecoder(resp.Body).Decode(&result)
return &result, nil
}
Lightweight alternative to /measurements. Returns compact logpile summaries without individual log sections; ideal for list views and map rendering.
Query parameters
Same as GET /measurements.
Response 200
Same envelope as GET /measurements, with compact logpile summaries in data. Also includes the optional deletedIds/resyncRequired fields (added in 1.1) with the same rules as GET /measurements.
Example with ETag caching
// Simple in-memory ETag store - use Redis / DB in production
const etagCache = new Map<string, string>();
app.get('/measurements/overview', async (req, res) => {
const params = new URLSearchParams({
limit: String(req.query.limit ?? 20),
offset: String(req.query.offset ?? 0),
sortBy: String(req.query.sortBy ?? 'createdAt'),
sortOrder: String(req.query.sortOrder ?? 'DESC'),
});
const cacheKey = params.toString();
const headers: Record<string, string> = { 'X-API-KEY': API_KEY };
const storedEtag = etagCache.get(cacheKey);
if (storedEtag) headers['If-None-Match'] = storedEtag;
const response = await fetch(`${API_BASE}/measurements/overview?${params}`, { headers });
if (response.status === 304) return res.status(304).end();
if (!response.ok) return res.status(response.status).json(await response.json());
const etag = response.headers.get('etag');
if (etag) etagCache.set(cacheKey, etag);
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.Optional;
public Optional<ResponseEntity<?>> getMeasurementsOverview(
int limit, int offset, String sortBy, String sortOrder, String etag) {
return restClient.get()
.uri(u -> u.path("/measurements/overview")
.queryParam("limit", limit)
.queryParam("offset", offset)
.queryParam("sortBy", sortBy)
.queryParam("sortOrder", sortOrder)
.build())
.headers(h -> { if (etag != null) h.setIfNoneMatch(etag); })
.exchange((req, resp) -> {
if (resp.getStatusCode() == HttpStatus.NOT_MODIFIED) {
return Optional.empty();
}
return Optional.of(resp.bodyTo(new ParameterizedTypeReference<>() {}));
});
}
# Inside ForstifyClient - see GET /version for full class setup
# Returns (data, new_etag). data is None on 304 Not Modified.
from typing import Optional
def get_measurements_overview(
self,
limit: int = 20,
offset: int = 0,
sort_by: str = "createdAt",
sort_order: str = "DESC",
etag: Optional[str] = None,
) -> tuple[Optional[dict], Optional[str]]:
headers = {}
if etag:
headers["If-None-Match"] = etag
resp = self._session.get(
f"{self.BASE_URL}/measurements/overview",
params={"limit": limit, "offset": offset, "sortBy": sort_by, "sortOrder": sort_order},
headers=headers,
)
if resp.status_code == 304:
return None, etag
resp.raise_for_status()
return resp.json(), resp.headers.get("ETag")
// Inside Client - see GET /version for full struct setup
// Returns (result, newEtag, error). result is nil on 304 Not Modified.
func (c *Client) GetMeasurementsOverview(
limit, offset int, sortBy, sortOrder, etag string,
) (*PaginatedResult, string, error) {
q := url.Values{}
q.Set("limit", strconv.Itoa(limit))
q.Set("offset", strconv.Itoa(offset))
q.Set("sortBy", sortBy)
q.Set("sortOrder", sortOrder)
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/measurements/overview?"+q.Encode(), nil)
req.Header.Set("X-API-KEY", c.apiKey)
if etag != "" {
req.Header.Set("If-None-Match", etag)
}
resp, err := c.http.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotModified {
return nil, etag, nil
}
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("forstify: %s", resp.Status)
}
newEtag := resp.Header.Get("ETag")
var result PaginatedResult
json.NewDecoder(resp.Body).Decode(&result)
return &result, newEtag, nil
}
Look up a single measurement by internal UUID or the human visible logpile number. At least one parameter is required.
Query parameters
| Parameter | Type | Description |
|---|---|---|
id | GUID | Internal measurement ID |
number | string (max 65) | Human visible logpile number |
Response 200
Single Measurement object.
Example
app.get('/measurements/search', async (req, res) => {
const { id, number } = req.query as Record<string, string>;
if (!id && !number) {
return res.status(400).json({ message: 'id or number required' });
}
const params = new URLSearchParams();
if (id) params.set('id', id);
if (number) params.set('number', number);
const response = await fetch(`${API_BASE}/measurements/search?${params}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 304) return res.status(304).end();
if (response.status === 404) return res.status(404).json({ message: 'not found' });
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
public Map<String, Object> searchMeasurement(String id, String number) {
if (id == null && number == null) {
throw new IllegalArgumentException("id or number is required");
}
UriComponentsBuilder uri = UriComponentsBuilder.fromPath("/measurements/search");
if (id != null) uri.queryParam("id", id);
if (number != null) uri.queryParam("number", number);
return restClient.get()
.uri(uri.build().toUri())
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
from typing import Optional
def search_measurement(
self, id: Optional[str] = None, number: Optional[str] = None
) -> dict:
if not id and not number:
raise ValueError("id or number is required")
params = {}
if id: params["id"] = id
if number: params["number"] = number
resp = self._session.get(f"{self.BASE_URL}/measurements/search", params=params)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
// Inside Client - see GET /version for full struct setup
func (c *Client) SearchMeasurement(id, number string) (map[string]any, error) {
if id == "" && number == "" {
return nil, fmt.Errorf("id or number is required")
}
q := url.Values{}
if id != "" { q.Set("id", id) }
if number != "" { q.Set("number", number) }
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/measurements/search?"+q.Encode(), nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
Returns one measurement by its internal UUID, with the same full detail as an item of GET /measurements. Use GET /measurements/search to look one up by logpile number instead.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | GUID | Internal measurement ID. A malformed value returns 400; an unknown id, or one that isn't visible to this account, returns 404. |
Response 200
Single Measurement object, same shape as one item of data in GET /measurements. Includes an ETag header; send it as If-None-Match to get 304 Not Modified when nothing changed. Costs 1 quota unit, 60 req / 60 s (default).
Example
app.get('/measurements/:id', async (req, res) => {
const response = await fetch(`${API_BASE}/measurements/${req.params.id}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 404) return res.status(404).json({ message: 'not found' });
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
import org.springframework.core.ParameterizedTypeReference;
import java.util.Map;
public Map<String, Object> getMeasurement(String id) {
return restClient.get()
.uri("/measurements/{id}", id)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
def get_measurement(self, id: str) -> dict:
resp = self._session.get(f"{self.BASE_URL}/measurements/{id}")
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
// Inside Client - see GET /version for full struct setup
func (c *Client) GetMeasurement(id string) (map[string]any, error) {
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/measurements/"+id, nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
Returns all wood lists belonging to the authenticated user, including all nested logpile measurements, paginated.
Query parameters
| Parameter | Type | Default | Options | Description |
|---|---|---|---|---|
limit | integer | 20 | 1 through 100 | Items per page |
offset | integer | 0 | 0 through 100000 | Pagination offset |
sortBy | string | createdAt | createdAt updatedAt title | Sort field |
sortOrder | string | ASC | ASC DESC | Sort direction |
updatedAfter | ISO 8601 datetime | n/a | n/a | Only records updated after this timestamp, for delta sync |
Response 200
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-06-01T08:15:00.000Z",
"title": "Schlag Nordost 2024",
"species": "fi",
"assortment": "Stammholz",
"grade": "st",
"volume": 148.2,
"logCount": 612,
"minLogLength": 3.0,
"maxLogLength": 5.0,
"latitude": 47.8123,
"longitude": 13.0456,
"images": ["https://..."],
"logpiles": [...]
}
],
"pagination": { "count": 1, "limit": 20, "offset": 0, "total": 12 },
"deletedIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
}
deletedIds (optional, added in 1.1): only present when updatedAfter is supplied, and only on the first page (offset=0) — ids of wood lists deleted since that timestamp.
resyncRequired (optional, added in 1.1): present instead of deletedIds in three cases — the deletion set exceeds the 1000-id per-request cap, wood-list deletion tombstones (kept for only 90 days) no longer cover the given updatedAfter, or updatedAfter is supplied with a team token (see Personal and team tokens). Drop updatedAfter and perform a full sync when you see this.
Example
app.get('/woodlists', async (req, res) => {
const params = new URLSearchParams({
limit: String(req.query.limit ?? 20),
offset: String(req.query.offset ?? 0),
sortBy: String(req.query.sortBy ?? 'createdAt'),
sortOrder: String(req.query.sortOrder ?? 'DESC'),
});
const response = await fetch(`${API_BASE}/woodlists?${params}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 304) return res.status(304).end();
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
public Map<String, Object> getWoodLists(
int limit, int offset, String sortBy, String sortOrder) {
return restClient.get()
.uri(u -> u.path("/woodlists")
.queryParam("limit", limit)
.queryParam("offset", offset)
.queryParam("sortBy", sortBy)
.queryParam("sortOrder", sortOrder)
.build())
.retrieve()
.body(new org.springframework.core.ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
def get_woodlists(
self,
limit: int = 20,
offset: int = 0,
sort_by: str = "createdAt",
sort_order: str = "DESC",
) -> dict:
resp = self._session.get(
f"{self.BASE_URL}/woodlists",
params={"limit": limit, "offset": offset, "sortBy": sort_by, "sortOrder": sort_order},
)
resp.raise_for_status()
return resp.json()
// Inside Client - see GET /version for full struct setup
func (c *Client) GetWoodLists(limit, offset int, sortBy, sortOrder string) (*PaginatedResult, error) {
q := url.Values{}
q.Set("limit", strconv.Itoa(limit))
q.Set("offset", strconv.Itoa(offset))
q.Set("sortBy", sortBy)
q.Set("sortOrder", sortOrder)
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/woodlists?"+q.Encode(), nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result PaginatedResult
json.NewDecoder(resp.Body).Decode(&result)
return &result, nil
}
Lightweight alternative to /woodlists. Returns compact wood list summaries without nested logpile detail.
Query parameters
Same as GET /woodlists.
Response 200
Same envelope as GET /woodlists, with compact wood-list summaries in data. Also includes the optional deletedIds/resyncRequired fields (added in 1.1) with the same rules as GET /woodlists.
Example
app.get('/woodlists/overview', async (req, res) => {
const params = new URLSearchParams({
limit: String(req.query.limit ?? 20),
offset: String(req.query.offset ?? 0),
sortBy: String(req.query.sortBy ?? 'createdAt'),
sortOrder: String(req.query.sortOrder ?? 'DESC'),
});
const response = await fetch(`${API_BASE}/woodlists/overview?${params}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 304) return res.status(304).end();
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
public Map<String, Object> getWoodListsOverview(
int limit, int offset, String sortBy, String sortOrder, String etag) {
return restClient.get()
.uri(u -> u.path("/woodlists/overview")
.queryParam("limit", limit)
.queryParam("offset", offset)
.queryParam("sortBy", sortBy)
.queryParam("sortOrder", sortOrder)
.build())
.headers(h -> { if (etag != null) h.setIfNoneMatch(etag); })
.retrieve()
.body(new org.springframework.core.ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
from typing import Optional
def get_woodlists_overview(
self,
limit: int = 20,
offset: int = 0,
sort_by: str = "createdAt",
sort_order: str = "DESC",
etag: Optional[str] = None,
) -> tuple[Optional[dict], Optional[str]]:
headers = {}
if etag:
headers["If-None-Match"] = etag
resp = self._session.get(
f"{self.BASE_URL}/woodlists/overview",
params={"limit": limit, "offset": offset, "sortBy": sort_by, "sortOrder": sort_order},
headers=headers,
)
if resp.status_code == 304:
return None, etag
resp.raise_for_status()
return resp.json(), resp.headers.get("ETag")
// Inside Client - see GET /version for full struct setup
func (c *Client) GetWoodListsOverview(
limit, offset int, sortBy, sortOrder, etag string,
) (*PaginatedResult, string, error) {
q := url.Values{}
q.Set("limit", strconv.Itoa(limit))
q.Set("offset", strconv.Itoa(offset))
q.Set("sortBy", sortBy)
q.Set("sortOrder", sortOrder)
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/woodlists/overview?"+q.Encode(), nil)
req.Header.Set("X-API-KEY", c.apiKey)
if etag != "" {
req.Header.Set("If-None-Match", etag)
}
resp, err := c.http.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotModified {
return nil, etag, nil
}
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("forstify: %s", resp.Status)
}
newEtag := resp.Header.Get("ETag")
var result PaginatedResult
json.NewDecoder(resp.Body).Decode(&result)
return &result, newEtag, nil
}
Look up a single wood list by internal UUID, public share ID, or exact title. At least one parameter is required.
Query parameters
| Parameter | Type | Description |
|---|---|---|
id | GUID | Internal wood list ID |
shareId | string | Public share ID |
title | string (max 65) | Exact title match |
Response 200
Single WoodList object.
Example
app.get('/woodlists/search', async (req, res) => {
const { id, shareId, title } = req.query as Record<string, string>;
if (!id && !shareId && !title) {
return res.status(400).json({ message: 'id, shareId or title required' });
}
const params = new URLSearchParams();
if (id) params.set('id', id);
if (shareId) params.set('shareId', shareId);
if (title) params.set('title', title);
const response = await fetch(`${API_BASE}/woodlists/search?${params}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 304) return res.status(304).end();
if (response.status === 404) return res.status(404).json({ message: 'not found' });
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
public Map<String, Object> searchWoodList(String id, String shareId, String title) {
if (id == null && shareId == null && title == null) {
throw new IllegalArgumentException("id, shareId or title is required");
}
UriComponentsBuilder uri = UriComponentsBuilder.fromPath("/woodlists/search");
if (id != null) uri.queryParam("id", id);
if (shareId != null) uri.queryParam("shareId", shareId);
if (title != null) uri.queryParam("title", title);
return restClient.get()
.uri(uri.build().toUri())
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
from typing import Optional
def search_woodlist(
self,
id: Optional[str] = None,
share_id: Optional[str] = None,
title: Optional[str] = None,
) -> Optional[dict]:
if not id and not share_id and not title:
raise ValueError("id, share_id or title is required")
params = {}
if id: params["id"] = id
if share_id: params["shareId"] = share_id
if title: params["title"] = title
resp = self._session.get(f"{self.BASE_URL}/woodlists/search", params=params)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
// Inside Client - see GET /version for full struct setup
func (c *Client) SearchWoodList(id, shareID, title string) (map[string]any, error) {
if id == "" && shareID == "" && title == "" {
return nil, fmt.Errorf("id, shareID or title is required")
}
q := url.Values{}
if id != "" { q.Set("id", id) }
if shareID != "" { q.Set("shareId", shareID) }
if title != "" { q.Set("title", title) }
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/woodlists/search?"+q.Encode(), nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
Returns one wood list by its internal UUID, with the same full detail as an item of GET /woodlists. Use GET /woodlists/search to look one up by title or share id instead.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | GUID | Internal wood list ID. A malformed value returns 400; an unknown id, or one that isn't visible to this account, returns 404. |
Response 200
Single WoodList object, same shape as one item of data in GET /woodlists. Includes an ETag header; send it as If-None-Match to get 304 Not Modified when nothing changed. Costs 1 quota unit, 60 req / 60 s (default).
Example
app.get('/woodlists/:id', async (req, res) => {
const response = await fetch(`${API_BASE}/woodlists/${req.params.id}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (response.status === 404) return res.status(404).json({ message: 'not found' });
if (!response.ok) return res.status(response.status).json(await response.json());
res.json(await response.json());
});
// Inside ForstifyClient - see GET /version for full class setup
import org.springframework.core.ParameterizedTypeReference;
import java.util.Map;
public Map<String, Object> getWoodList(String id) {
return restClient.get()
.uri("/woodlists/{id}", id)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
# Inside ForstifyClient - see GET /version for full class setup
def get_wood_list(self, id: str) -> dict:
resp = self._session.get(f"{self.BASE_URL}/woodlists/{id}")
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
// Inside Client - see GET /version for full struct setup
func (c *Client) GetWoodList(id string) (map[string]any, error) {
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/woodlists/"+id, nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
Downloads a measurement/logpile image as JPEG binary, by the blob id referenced in Measurement.images or a wood list's overview image.id. Ownership is checked; the image must belong to a measurement the token's user can access.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | GUID | Image blob id |
Response 200
image/jpeg binary. Cache-Control: public,max-age=86400,immutable. Costs 3 quota units, 60 req / 60 s.
Example
app.get('/images/:id', async (req, res) => {
const response = await fetch(`${API_BASE}/images/${req.params.id}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (!response.ok) return res.status(response.status).json(await response.json());
res.setHeader('Content-Type', 'image/jpeg');
res.send(Buffer.from(await response.arrayBuffer()));
});
// Inside ForstifyClient - see GET /version for full class setup
public byte[] getImage(String id) {
return restClient.get()
.uri("/images/{id}", id)
.retrieve()
.body(byte[].class);
}
# Inside ForstifyClient - see GET /version for full class setup
def get_image(self, id: str) -> bytes:
resp = self._session.get(f"{self.BASE_URL}/images/{id}")
resp.raise_for_status()
return resp.content
// Inside Client - see GET /version for full struct setup
func (c *Client) GetImage(id string) ([]byte, error) {
req, _ := http.NewRequest(http.MethodGet, c.baseURL+"/images/"+id, nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
return io.ReadAll(resp.Body)
}
Same as GET /images/:id, resized on the server to the given dimensions; use for thumbnails instead of downloading images at full resolution.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | GUID | Image blob id |
width | integer | Target width in px |
height | integer | Target height in px |
Response 200
image/jpeg binary, resized. Cache-Control: public,max-age=86400,immutable. Costs 3 quota units, 60 req / 60 s.
Example
app.get('/images/:id/:width/:height', async (req, res) => {
const { id, width, height } = req.params;
const response = await fetch(`${API_BASE}/images/${id}/${width}/${height}`, {
headers: { 'X-API-KEY': API_KEY },
});
if (!response.ok) return res.status(response.status).json(await response.json());
res.setHeader('Content-Type', 'image/jpeg');
res.send(Buffer.from(await response.arrayBuffer()));
});
// Inside ForstifyClient - see GET /version for full class setup
public byte[] getScaledImage(String id, int width, int height) {
return restClient.get()
.uri("/images/{id}/{width}/{height}", id, width, height)
.retrieve()
.body(byte[].class);
}
# Inside ForstifyClient - see GET /version for full class setup
def get_scaled_image(self, id: str, width: int, height: int) -> bytes:
resp = self._session.get(f"{self.BASE_URL}/images/{id}/{width}/{height}")
resp.raise_for_status()
return resp.content
// Inside Client - see GET /version for full struct setup
func (c *Client) GetScaledImage(id string, width, height int) ([]byte, error) {
path := fmt.Sprintf("/images/%s/%d/%d", id, width, height)
req, _ := http.NewRequest(http.MethodGet, c.baseURL+path, nil)
req.Header.Set("X-API-KEY", c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("forstify: %s", resp.Status)
}
return io.ReadAll(resp.Body)
}
Data types
Measurement
Full logpile recording, returned by /measurements and /measurements/search.
| Field | Type | Description |
|---|---|---|
id | string | UUID |
createdAt | string | ISO 8601 UTC |
updatedAt | string | ISO 8601 UTC; use as the cursor for delta sync |
logpileNumber | string | null | Human visible logpile number |
species | WoodSpecies | null | |
assortment | WoodAssortment | null | |
variety | WoodVariety | null | |
logCount | number | null | Total number of logs |
logpileWidth | number | null | Logpile width in metres |
latitude | number | null | GPS latitude |
longitude | number | null | GPS longitude |
notes | string | null | Max 500 chars |
images | string[] | null | Absolute image URLs |
volumes | Volume[] | null | Volume breakdown by quality class |
singleLogs | SingleLog[] | null | Individual log data, present only on measurements with individual logs |
metadata | MeasurementMetadata | null | Forestry metadata fields |
WoodList
Full wood list including nested logpile measurements, returned by /woodlists and /woodlists/search.
| Field | Type | Description |
|---|---|---|
id | string | UUID |
createdAt | string | ISO 8601 UTC |
updatedAt | string | ISO 8601 UTC; use as the cursor for delta sync |
title | string | null | Max 65 chars |
notes | string | null | Max 500 chars |
species | WoodSpecies | null | |
assortment | WoodAssortment | null | |
variety | WoodVariety | null | |
grade | WoodGrade | null | Wood assortment grade |
volume | number | null | Total m³ across all logpiles |
logCount | number | null | Total log count |
minDiameter / maxDiameter | number | null | cm |
minLogLength / maxLogLength | number | null | metres |
minQuality / maxQuality | WoodQuality | null | |
latitude / longitude | number | null | Centroid of all logpile coordinates |
images | string[] | null | Absolute image URLs |
logpiles | Measurement[] | null | All nested logpile measurements |
LogpileShort (overview endpoint)
| Field | Type | Description |
|---|---|---|
id | string | UUID |
number | string | Human visible logpile number |
assortment | WoodAssortment | null | |
grade | WoodAssortmentFrontend | null | Despite the name, this is the assortment enum, not WoodGrade |
variety | WoodVariety | null | Unlike other fields in this table, always present — emitted as null rather than omitted when absent |
volume | number | null | m³ |
cubicMeter | number | null | Rm; only set for measurements of type ARM |
logCount | number | null | |
logLength | number | null | Average in metres |
logLengthRange | { min, max } | null | metres |
logDiameter | { min, max } | null | cm |
location | WoodLocationDto | null | Street, house number, postcode, city, and coordinate: { latitude, longitude }; see WoodLocationDto |
notes | string | null |
WoodListShort (overview endpoint)
| Field | Type | Description |
|---|---|---|
id | string | UUID |
title | string | null | |
species | WoodSpecies | null | |
assortment | WoodAssortment | null | |
variety | WoodVariety | null | |
grade | WoodGrade | null | |
logLength | number | null | Average in metres |
volume | number | null | Total m³ |
logLengthRange | { min, max } | null | metres |
logDiameter | { min, max } | null | cm |
quality | { min, max } | null | WoodQuality values |
location | WoodLocationDto | null | City and coordinate / approximateCoordinate: { latitude, longitude }; see WoodLocationDto |
image | { id, width?, height? } | null | Title image reference; fetch the binary via GET /images/:id using id |
bundleOfferId, but it is not exposed on this DTO; it never appears in the API response.
Volume
One quality / length class within a logpile measurement.
| Field | Type | Description |
|---|---|---|
number | number | Sort index |
quality | WoodQuality | |
logLength | number | metres |
volume | number | m³. For a sectional measurement with a backside this is the mean of the front and back volumes. |
frontsideVolume | number | null | m³, front side. Only present on sectional measurements (omitted on other volume rows); null when there is no value. Use backsideVolume to detect a backside. |
backsideVolume | number | null | m³, back side. Only present on sectional measurements (omitted on other volume rows); null when no backside exists. |
logCount | number | null |
SingleLog / SingleLogSection
Individual log data, present only on measurements with individual logs.
SingleLog { number: string; sections: SingleLogSection[] } // free text label, not always numeric
SingleLogSection {
number: number // sort index
logLength: number // metres
quality: WoodQuality
diameter: number // cm
volume: number // m³
}
WoodLocationDto
Used for location on the overview endpoints. Populated fields vary; logpile overviews set street level detail, wood list overviews only city and coordinates.
| Field | Type | Description |
|---|---|---|
street | string | |
houseNumber | string | |
postcode | string | |
city | string | |
coordinate | { latitude, longitude } | Exact GPS coordinate |
approximateCoordinate | { latitude, longitude } | Obfuscated GPS coordinate (privacy) |
MeasurementMetadata
All fields are string | null, max 100 chars each.
| Field | Field |
|---|---|
forestOwner | locationStreet |
forester | locationHouseNumber |
serviceProvider | locationPostcode |
buyer | locationCity |
hiebsNumber | locationCountry |
areaNumber | inventoryUnit |
lotNumber | division |
subdivision |
Enum reference
| Value | Meaning |
|---|---|
Stammholz | Round timber |
Energieholz | Energy wood |
Industrieholz | Industrial wood |
Sondersortiment | Special assortment |
Palettenholz | Pallet wood |
OSB | OSB |
| Value | Corresponds to |
|---|---|
ch_s | WoodAssortment.Stammholz |
ch_e | WoodAssortment.Energieholz |
ch_i | WoodAssortment.Industrieholz |
ch_r | WoodAssortment.Sondersortiment |
all | All assortments selected |
| Value | Meaning |
|---|---|
Lang | Long |
Kurz | Short |
Abschnitte | Sections |
Waldhackschnitzel | Forest chips |
Andere | Other |
| Value | Standard |
|---|---|
a b c d | DE quality classes |
ch_ab ch_abc | CH quality classes |
bc bcd cd | Combined |
in | Industrial, normal |
ik | Industrial, diseased |
if | Industrial, defective |
nf nfk fk | Industrial combined |
| Value | Meaning |
|---|---|
st | Stammholz lang |
fl | Stammholz Abschnitte |
il | Industrieholz lang |
is | Industrieholz kurz |
bl | Energieholz lang |
bs | Energieholz kurz |
s_hs e_hs i_hs | Hackschnitzel |
ol os o_fl o_hs o | Sondersortiment |
pa_l pa_s pa_hs | Palettenholz |
WoodSpecies, full list
All 128 values accepted for species. Species with sub varieties (e.g. individual spruce/pine/oak types) roll up to a parent code (fi, kie, ta, ...) when the exact variety isn't tracked.
| Value | DE | EN |
|---|---|---|
xy | Laub- & Nadelholz | Hardwood and Softwood |
ndh | Nadelholz | Softwood |
fi | Fichte | Spruce |
gfi | Gemeine Fichte | Common Spruce |
ofi | Omorikafichte | Serbian Spruce |
sfi | Sitkafichte | Sitka Spruce |
swfi | Schwarzfichte | Black Spruce |
efi | Engelmannsfichte | Engelmann Spruce |
bfi | Blaufichte/Stechfichte | Blue Spruce / Norway Spruce |
wfi | Weißfichte | White Spruce |
sofi | Sonstige Fichten | Other Spruce |
kie | Kiefer | Pine |
ki | Gemeine Kiefer | Common Pine |
bki | Bergkiefer | Mountain Pine |
ski | Schwarzkiefer | Black Pine |
rki | Rumelische Kiefer | Balkan Pine |
zki | Zirbelkiefer | Stone Pine |
wki | Weymouthskiefer | Weymouth Pine |
mki | Murraykiefer | Murray Pine |
gki | Gelbkiefer | Ponderosa Pine |
soki | Sonstige Kiefer | Other Pine |
ta | Tanne | Fir |
wta | Weißtanne | Silver Fir |
ata | Amerikanische Edeltanne | American Silver Fir |
cta | Coloradotanne | Colorado Fir |
kta | Küstentanne | Coastal Fir |
nita | Nikkotanne | Nikko Fir |
nota | Nordmannstanne | Nordmann Fir |
vta | Veitchtanne | St. Vitus Fir |
sota | Sonstige Tannen | Other Firs |
dgl | Douglasie | Douglas Fir |
la | Lärche | Larch |
ela | Europäische Lärche | European Larch |
jla | Japanische Lärche Hybrid | Japanese Larch Hybrid |
sla | Sonstige Lärchen | Other Larches |
sonb | Sonstige Nadelbäume | Other Conifers |
lb | Lebensbaum | Arborvitae / Thuja |
ht | Hemlockstanne | Hemlock |
mam | Mammutbaum | Sequoia / Redwood |
eib | Eibe | Yew |
sz | Lawsonszypresse | Lawson Cypress |
bu | Buche | Beech |
sei | Stieleiche | Common Oak |
tei | Traubeneiche | Sessile Oak |
rei | Roteiche | Red Oak |
zei | Zerreiche | Turkey Oak |
suei | Sumpfeiche | Swamp Oak |
ei | Eiche | Oak |
que | sonstige Eichen | Other Oaks |
es | Esche | Ash |
ges | Gemeine Esche | Common Ash |
wes | Weißesche | White Ash |
fra | Sonstige Eschen | Other Ash |
hbu | Hainbuche / Weißbuche | Hornbeam |
ah | Ahorn | Maple |
bah | Bergahorn | Sycamore Maple |
sah | Spitzahorn | Norway Maple |
fah | Feldahorn | Field Maple |
eah | Eschenblättriger Ahorn | Ash-leaved Maple |
siah | Silberahorn | Silver Maple |
ace | Sonstige Ahorne | Other Maples |
li | Linde | Lime |
wli | Winterlinde | Small-Leaved Lime |
sli | Sommerlinde | Summer Lime |
til | Sonstige Linden | Other Lime Trees |
rob | Robinie | Black Locust |
akz | Akazie | Acacia |
ul | Ulme | Elm |
bul | Bergulme | Wych Elm |
ful | Feldulme | Field Elm |
flu | Flatterulme | White Elm |
ulm | Sonstige Ulmen | Other Elms |
rka | Rosskastanie | Horse Chestnut |
eka | Edelkastanie | Sweet Chestnut |
ka | Kastanie | Chestnut |
mau | Weißer Maulbeerbaum | White Mulberry |
nus | Nussbaum | Walnut |
wnu | Walnuss | Walnut |
snu | Schwarznuss Hybrid | Black Walnut Hybrid |
jug | Sonstige Nussbäume | Other Nut Trees |
ste | Stechpalme | Holly |
pla | Platane | Sycamore |
apl | Ahornblättrige Platane | London Plane |
solh | Sonstige Laubbäume mit hoher Lebensdauer | Other deciduous trees with a long lifespan |
gbi | Gemeine Birke | Common Birch |
mbi | Moorbirke / Karpatenbirke | Bog Birch / Carpathian Birch |
bi | Birke | Birch |
erl | Erle | Alder |
ser | Schwarzerle | Black Alder |
wer | Weißerle / Grauerle | White Alder / Gray Alder |
ger | Grünerle | Green Alder |
aln | Sonstige Erlen | Other Alders |
pap | Pappel | Poplar |
zpa | Aspe / Zitterpappel | Aspen / Trembling Poplar |
spa | Europäische Schwarzpappel | European Black Poplar |
spah | Schwarzpappel Hybrid | Black Poplar Hybrid |
gpa | Graupappel Hybrid | Gray Poplar Hybrid |
wpa | Silberpappel / Weißpappel | Silver Poplar / White Poplar |
bpa | Balsampappel | Balsam Poplar |
bpah | Balsampappel Hybrid | Balsam Poplar Hybrid |
pop | Sonstige Pappeln | Other Poplars |
sor | Sorbusarten | Sorbus Species |
sso | Sonstige Sorbusarten | Other Sorbus Species |
vb | Vogelbeere | Rowan Berry |
els | Elsbeere | Serviceberry |
spe | Speierling | Service Tree |
meb | Echte Mehlbeere | Real Serviceberry |
wei | Weide | Willow |
swei | Salweide | Sal Willow |
kir | Kirsche | Cherry |
gtk | Gew. Traubenkirsche | Common Bird Cherry |
vk | Vogelkirsche | Wild Cherry / Bird Cherry |
stk | Spätbl. Traubenkirsche | Black Cherry |
pru | Sonstige Kirschen | Other Cherries |
zwe | Zwetschge | Damson |
hic | Hickory | Hickory |
soln | Sonstige Laubbäume mit niedriger Lebensdauer | Other deciduous trees with a short lifespan |
fau | Gemeiner Faulbaum / Pulverholz | Common Buckthorn / Powder Wood |
wob | Wildobst (unbestimmt) | Wild fruit (undefined) |
wap | Holzapfel / Wildapfel | Crab Apple / Wild Apple |
wbi | Holzbirne / Wildbirne | Wood Pear / Wild Pear |
has | Baumhasel | Turkish Hazel |
got | Gem. Götterbaum | Common Tree Of Heaven |
slbh | Sonstiges Hartlaubholz | Other Hardwood |
lbh | Laubholz | Hardwood |
slbw | Sonstiges Weichlaubholz | Other Softwood |
str | Strauch (unbestimmt) | Shrub (undefined) |
fita | Mischsortiment Fichte/Tanne | Mixed Assortment Spruce/Fir |