Startnext API for Developers

The Startnext API allows you to integrate project data into your website, app, or other applications. This documentation shows you how to use the API.

Quick Start

The Startnext API uses two types of API keys for external access:

Key type Prefix Use case
Project API Key snx_proj_* Access data of a specific project you manage
User API Key snx_user_* Access your own projects programmatically

1a. Create a Project API Key

To access data for a specific project, you need a Project API Key:

  1. Open your project on Startnext
  2. Go to Project InterfaceAdministrationAPI Keys
  3. Click Create New API Key
  4. Select the required permissions (scopes)
  5. Copy the displayed API key and store it securely

Important: The full API key is only shown once at creation. It starts with snx_proj_ followed by a random string.

1b. Create a User API Key

To access your own projects programmatically, you need a User API Key:

  1. Log in to Startnext
  2. Go to SettingsAPI & Integrations
  3. Click Create New API Key
  4. Enter a name — the user:read scope is always included automatically
  5. Copy the displayed API key — it starts with snx_user_

Important: The full API key is only shown once at creation.

2. First API Request

# Project-specific data (Project API Key)
curl -H "Authorization: Bearer snx_proj_YOUR_API_KEY" \
     "https://www.startnext.com/myty/api/crowdfunding/project/your-project"

# Your own and team projects (User API Key)
curl -H "Authorization: Bearer snx_user_YOUR_API_KEY" \
     "https://www.startnext.com/myty/api/crowdfunding/user/me/projects"

Terms of Use

By using the Startnext API, you agree to the Startnext API Terms of Use.

Base URL

All API endpoints start with:

https://www.startnext.com/myty/api/crowdfunding

Authentication

Add your API key as a Bearer Token in the Authorization header:

Authorization: Bearer snx_proj_xxxxxxxxxxxxxxxx

Authorization: Bearer is the only accepted channel.

Keeping Your API Key Secret

An API key is a credential, not a public identifier. Anyone who has it can read everything its scopes allow — with orders:read or contacts:read that includes the personal data of your supporters.

Never put a key anywhere a visitor can reach it:

A browser request that works is not a request that is safe. Key-authenticated responses carry Access-Control-Allow-Origin: *, so a fetch() with an Authorization header does succeed from any website — while shipping your key to every visitor, who can read it in the browser's network tab. Consider a key that has ever been in browser-side code compromised: delete it under Administration → API Keys and create a new one.

Store the key in a protected environment variable of your hosting provider instead:

STARTNEXT_PROJECT_API_KEY=snx_proj_xxxxxxxxxxxxxxxx
STARTNEXT_PROJECT_SLUG=my-project

Call the API from your own backend

Put a small endpoint of your own between your website and the Startnext API. A server, a CMS backend or a serverless function all work:

flowchart TD
    A["Website in the browser"] --> B["Your own public endpoint"]
    B --> C["Cache, approx. 60 seconds"]
    C --> D["Startnext API, called with the secret key"]

Your backend reads the key from the environment, pages through all results, drops what must not be public (see List Rewards), caches the response for about 60 seconds and returns only the fields your page needs. See Display Rewards in Your Own Shop for a complete example of both halves.

This keeps the key on your server, and it stops a busy page from turning every single visit into a Startnext request — which is what the rate limits are counted against.

Available Permissions (Scopes)

A note on wording: what this guide calls a reward is named incentive throughout the API — in paths (/incentives), scopes (incentives:read) and response schemas. Both refer to the same thing: what a project offers its supporters in return for their support. You'll see both side by side in the scope table right below.

Project API Key Scopes

When creating a Project API Key, select which project data may be accessed:

Scope Description
blog:read Blog posts/updates of the project
fundings:read Public supports for streams (timestamp and name)
incentives:read Project rewards
orders:read Orders (supports) with full details
project:read Basic project data (title, description, funding status)
subscribers:read Subscribers of the project
wall:read Wall posts of the project

Note: Project API Keys require your project to be on the Pro scope or higher. Basic/Legacy projects cannot create API keys.

User API Key Scopes

A User API Key is your personal, end-user credential — distinct from a Project API Key, which is scoped to a single project. When creating a User API Key, select which data may be accessed (user:read is always included and cannot be deselected):

Scope Always enabled Description
user:read Read your own profile/identity and team invitations (/user/me/identity) — always included
user:write Update your profile data, e.g. profile image (/user/me/image)
project:read Read your own and team projects incl. team, blog, timeline, rewards and funding (/user/me/projects, /user/me/incentives)
project:write Create and manage your own and team projects incl. team, blog, timeline, rewards and images (POST /project)
contacts:read Read personal contact data of supporters and subscribers (orders, subscriber emails)

API Endpoints

Retrieve Project Data

GET /project/{link_caption}

Scope: project:read

Returns the basic project information.

This is the one endpoint in this guide that also works without a credential. From project status started onward, it answers unauthenticated requests too — rate-limited per IP like any other unauthenticated request, and without the fields a key would add. That is what powers the embeddable funding widget: no API key needed for a public project. A Project API Key with project:read, or a User API Key belonging to the project's Owner, Manager or a Team member, additionally unlocks projects in status new (drafts) — for everything else, a request with or without a key returns the same public view.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-awesome-project"

Response:

{
    "project": {
        "id": 12345,
        "title": "My Awesome Project",
        "subtitle": "A project for everyone",
        "link_caption": "my-awesome-project",
        "status": "active",
        "currency": "EUR",
        "current_funding": 7550.00,
        "total_funding": 7550.00,
        "funding_threshold": 10000.00,
        "project_type_all_or_nothing": true,
        "supporter_count": 150,
        "like_count": 89,
        "teaser_text": "Short description...",
        "url": {
            "main": "https://www.startnext.com/my-awesome-project",
            "main_relative": "/my-awesome-project",
            "comments": "https://www.startnext.com/my-awesome-project/comments",
            "faq": "https://www.startnext.com/my-awesome-project/faq",
            "widget": "https://www.startnext.com/my-awesome-project/widget",
            "localized_urls": {
                "de": "/my-awesome-project",
                "en": "/en/my-awesome-project"
            }
        }
    },
    "status": 0
}

There is no url.support — build the support link from url.main. url.widget points at the ready-made embed widget, which is often simpler than building your own funding display.

The top-level status field is legacy. It is a hard-coded 0 on every successful response and carries no information — do not branch on it. Use the HTTP status code instead. Note that it is unrelated to project.status, which holds the project's actual phase (new, started, active, successful, …).


List Rewards

GET /project/{link_caption}/incentives

Scope: incentives:read

Returns all rewards of the project, including hidden ones (manually hidden via status=hidden or hidden due to an unmet funding goal). Use hidden_until_goal_reached and is_unlocked on each reward to determine its visibility state.

Parameters:

Parameter Type Description
limit Integer Maximum number of results (default: 20, max: 100)
offset Integer Starting position for pagination (default: 0)

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/incentives?limit=10"

Response:

{
    "data": [
        {
            "id": 12345,
            "title": "Limited T-Shirt",
            "description": "An exclusive T-shirt for supporters",
            "price": 25.00,
            "quantity_total": 100,
            "quantity_remaining": 42,
            "quantity_sold": 58,
            "status": "enabled",
            "address_required": true,
            "is_available": true,
            "is_unlocked": true,
            "hidden_until_goal_reached": false
        }
    ],
    "meta": {
        "total": 15,
        "limit": 10,
        "offset": 0
    }
}

Which rewards you may show publicly

This endpoint deliberately returns rewards that are not visible on the project page, so your own tooling can see the full picture. Filter them out before you publish them anywhere:

Condition Meaning
status is not enabled Manually hidden or disabled by the project — never show it. Only enabled and hidden occur in this response.
hidden_until_goal_reached: true and not yet unlocked Kept secret until the funding goal is reached — never show it.
function isPublic(reward) {
    if (reward.status !== 'enabled') return false;
    if (!reward.hidden_until_goal_reached) return true;

    return reward.is_unlocked === true;
}

A reward that has an unlock threshold but hidden_until_goal_reached: false is not secret: the project page shows it in a locked state, along with the amount still missing (funding_amount_until_unlock). Dropping it would display fewer rewards than Startnext itself does. So use is_unlocked to decide whether a reward can be ordered, not whether it may be shown.

Fields that are easy to misread

Field What to watch out for
quantity_total 0 means unlimited — not sold out.
quantity_remaining Only meaningful while quantity_total > 0. For unlimited rewards it carries the placeholder value 999; never print that as a stock figure.
is_available The flag to use for "still in stock". It already handles unlimited rewards. It says nothing about visibility or unlock state.
is_in_stock Not an availability switch. It is a label the project sets to signal that the reward ships immediately, and it is routinely false on rewards that are perfectly orderable.
description Contains HTML (paragraphs, links, lists). Sanitize it server-side or render it as plain text — never pass it into innerHTML unchecked.
price The currency is not part of the reward; it is on the embedded project object as project.currency.
url Absolute link to the reward on Startnext. Prefer it over a hand-built URL for your "support" button.

Reward images

A reward carries its images in gallery_images, an array of file objects. The older single image field is deprecated and is frequently null even when the reward clearly has pictures — so read gallery_images first and treat image only as a fallback.

Each file object offers two pregenerated sizes in thumbnails: 1x for normal displays and 2x for retina. Entries are not necessarily images, so filter on is_image before using one:

function getPreviewImage(reward) {
    const galleryImage = reward.gallery_images?.find(file => file.is_image);

    return galleryImage?.thumbnails?.['2x']
        ?? galleryImage?.thumbnails?.['1x']
        ?? reward.image?.thumbnails?.['2x']
        ?? reward.image?.thumbnails?.['1x']
        ?? null;
}

Two things to plan for:


Get a Single Reward

GET /project/{link_caption}/incentive/{id}

Scope: incentives:read

Returns one reward by ID, scoped to the project — same fields and same visibility rules as List Rewards above (including hidden/locked rewards; filter with status and hidden_until_goal_reached/is_unlocked before displaying one you fetched directly), plus its variation configuration, which List Rewards omits.

A reward has up to three independent variation groups (variant_label_1/variant_choices_1 through _3 in the API). A group with a null label is unused. A group with a label but an empty variant_choices_N array is free text — backers type their own value instead of picking from a list (e.g. a custom engraving). A non-empty variant_choices_N is a fixed, orderable set of IncentiveOption values (id, name, position, group) — see the API reference for the full schema.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/incentive/12345"

Response:

{
    "data": {
        "id": 12345,
        "title": "Limited T-Shirt",
        "description": "An exclusive T-shirt for supporters",
        "price": 25.00,
        "quantity_total": 100,
        "quantity_remaining": 42,
        "quantity_sold": 58,
        "status": "enabled",
        "address_required": true,
        "is_available": true,
        "is_unlocked": true,
        "hidden_until_goal_reached": false,
        "variant_label_1": "Color",
        "variant_choices_1": [
            {"id": 501, "name": "Black", "position": 1, "group": 1},
            {"id": 502, "name": "White", "position": 2, "group": 1}
        ],
        "variant_label_2": "Custom engraving",
        "variant_choices_2": [],
        "variant_label_3": null,
        "variant_choices_3": []
    }
}

List Blog Posts

GET /project/{link_caption}/blog

Scope: blog:read

Returns all blog posts/updates of the project.

Query parameters: blog_type (text|story, default text), offset (default 0), limit (default 20, max 100), include_supporter_only (0|1, default 0).

Supporter-only posts: Blog posts flagged is_supporter_only are always part of the list and of meta.total — only their content is gated. Without authorization they come back redacted: is_locked is true, description holds a short public teaser (max. 400 characters) and image, image_mobile, gallery_images, embed_url_13 and video are empty. The full content is returned when the request sets include_supporter_only=1 and the caller is verified server-side as allowed to read it (an OAuth owner/manager token, or a logged-in owner, team member, platform manager or supporter of the project). A public project API key alone never unlocks the content — it always receives the teaser. Requests with include_supporter_only=1 bypass the CDN cache.

reading_time_minutes is always computed from the full text, so it stays correct on a redacted entry.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/blog"

Response:

{
    "data": [
        {
            "id": 5678,
            "title": "We made it!",
            "text": "Thanks to all supporters...",
            "type": "text",
            "status": "enabled",
            "release_timestamp": 1705315800
        }
    ],
    "meta": {
        "total": 5,
        "limit": 20,
        "offset": 0
    }
}

List Orders

GET /project/{link_caption}/orders

Scope: orders:read

Note: This endpoint is only available once the project has been successfully funded (≥100%).

Returns all successful supports with full details.

When the data is final. Only supports with status success are returned. Until the project is paid out, a support can still leave that status (e.g. a failed collection) and then drops out of this list. After payout — process_status is payoff on the project — supports are no longer modified: a chargeback or a supporter withdrawing at that point is handled outside Startnext and does not change the support here. Use transaction_number as the key for matching, and updated_timestamp to detect changes while the project is still in collection.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/orders"

Response:

{
    "data": [
        {
            "support_timestamp": 1705315800,
            "updated_timestamp": 1705402200,
            "transaction_number": "TR1234567890ST",
            "total_amount": 50.00,
            "status_code": "collected",
            "bill_address": {
                "firstname": "Jane",
                "lastname": "Doe",
                "city": "Berlin",
                "country": "Germany"
            },
            "ordered_incentives": [
                {
                    "id": 12345,
                    "count": 2,
                    "price_single": 25.00,
                    "price_total": 50.00
                }
            ]
        }
    ],
    "meta": {
        "total": 150,
        "limit": 20,
        "offset": 0
    }
}

Field naming: the address objects use firstname / lastname, whereas user objects on other endpoints use first_name / last_name. This inconsistency exists in the API itself.


List Subscribers

GET /project/{link_caption}/subscribers

Scope: subscribers:read

Returns all subscribers of the project.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/subscribers"

Response:

{
    "data": [
        {
            "subscribed_timestamp": 1705315800,
            "is_news_subscribed": true,
            "user": {
                "id": 789,
                "first_name": "Jane",
                "last_name": "Doe",
                "is_public": true
            }
        }
    ],
    "meta": {
        "total": 89,
        "limit": 20,
        "offset": 0
    }
}

user may be null for subscribers without a Startnext account. Note that this endpoint returns first_name / last_name and has no display_name field.


List Wall Posts

GET /project/{link_caption}/wall

Scope: wall:read

Returns all wall posts of the project.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/wall"

Response:

{
    "data": [
        {
            "id": 12345,
            "message": "Great project! Good luck!",
            "status": "enabled",
            "created_timestamp": 1705315800,
            "user": {
                "id": 789,
                "first_name": "Jane",
                "last_name": "Doe",
                "display_name": "Jane Doe"
            }
        }
    ],
    "meta": {
        "total": 25,
        "limit": 20,
        "offset": 0
    }
}

Public Support Stream

GET /project/{link_caption}/fundings

Scope: fundings:read

Returns publicly visible supports with minimal data. Intended for live streams or feeds of recent supporters.

Note: For detailed order data (addresses, rewards, etc.), use the /orders endpoint with the orders:read scope instead.

Example:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/fundings"

Response:

{
    "data": [
        {
            "support_timestamp": 1705315800,
            "user": {
                "id": 789,
                "display_name": "Jane Doe"
            }
        }
    ],
    "meta": {
        "total": 150,
        "limit": 20,
        "offset": 0
    }
}

This endpoint returns only support_timestamp and user per entry — nothing else.


OpenAPI Specification

GET /crowdfunding/openapi.json

Auth: None (public endpoint)

Returns the complete OpenAPI specification for all public API endpoints (Project API Key and User API Key endpoints) as JSON. Useful for MCP clients, API explorers, and code generation.

This endpoint does not list itself. It exists to describe the rest of the API, so it is intentionally left out of its own output — do not expect a GET /openapi.json operation inside the returned document.

Example:

curl "https://www.startnext.com/myty/api/crowdfunding/openapi.json"

Get API Key Identity

GET /user/me/identity

Credential: User API Key Scope: user:read (always included in every credential)

Returns minimal identity data for the authenticated credential: the user ID, URL slug, and the list of active scopes. Contains no PII. Intended for verifying an end-user credential — e.g. by MCP clients — and for discovering which permissions it has.

Example:

curl -H "Authorization: Bearer snx_user_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/user/me/identity"

Response:

{
    "data": {
        "id": 42,
        "link_caption": "jane-doe",
        "scopes": [
            {
                "scope": "user:read",
                "endpoints": [
                    "GET /myty/api/crowdfunding/user/me/identity"
                ]
            },
            {
                "scope": "project:read",
                "endpoints": [
                    "GET /myty/api/crowdfunding/user/me/projects"
                ]
            }
        ]
    }
}

Get Own Profile

GET /user/me

Credential: User API Key Scope: user:read (always included in every credential)

Returns the profile of the authenticated user: name, display name, biography, city, company, profile image, counters (own, team, supported and liked projects) and the URLs of the personal areas.

Unlike /user/me/identity this response contains personal data — treat it accordingly and do not cache it alongside public project data.

Example:

curl -H "Authorization: Bearer snx_user_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/user/me"

Response (abridged):

{
    "user": {
        "id": 42,
        "display_name": "Jane Doe",
        "first_name": "Jane",
        "last_name": "Doe",
        "address_city": "Dresden",
        "biography": "…",
        "profile_image": "https://…",
        "own_projects_count": 2,
        "team_projects_count": 1,
        "supported_projects_count": 7,
        "registered_timestamp": 1712345678,
        "url": { "main": "https://www.startnext.com/jane-doe", "…": "…" }
    }
}

Get Rewards from Own and Team Projects

GET /user/me/incentives

Credential: User API Key Scope: project:read

Returns a paginated list of all active rewards from projects where the authenticated user is the initiator or a team member.

Parameters:

Parameter Type Description
sort String Sort order: sold (default), new, price-a, price-d, name-a, name-d, rand
fundable Boolean Filter by fundable projects only
is_favorite Boolean Filter favorites only
limit Integer Maximum number of results (default: 20, max: 100)
offset Integer Starting position for pagination (default: 0)

Example:

curl -H "Authorization: Bearer snx_user_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/user/me/incentives"

Response:

{
    "data": [
        {
            "id": 12345,
            "title": "Limited T-Shirt",
            "price": 25.00,
            "is_available": true,
            "quantity_total": 100,
            "quantity_remaining": 42,
            "project": {
                "id": 678,
                "title": "My Awesome Project",
                "link_caption": "my-awesome-project"
            }
        }
    ],
    "meta": {
        "total": 5,
        "limit": 20,
        "offset": 0
    }
}

List Own and Team Projects

GET /user/me/projects

Credential: User API Key Scope: project:read

Returns a paginated list of all projects where the authenticated user is the initiator or a team member — including drafts, regardless of the authentication path. Accepts a User API Key with the project:read scope, or a session cookie.

Parameters:

Parameter Type Description
sort String Sort order: project-end-date-d (default), project-end-date-a, project-activation-date-d, project-activation-date-a, project-title-d, project-title-a, project-funding-sum-d, project-funding-sum-a, project-support-count-d, project-support-count-a, project-fan-count-d, project-fan-count-a, rand
limit Integer Maximum number of results (default: 20, max: 100)
offset Integer Starting position for pagination (default: 0)

Example:

curl -H "Authorization: Bearer snx_user_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/user/me/projects?sort=project-activation-date-d&limit=20"

Response:

{
    "data": [
        {
            "id": 12345,
            "title": "My Awesome Project",
            "link_caption": "my-awesome-project",
            "status": "active",
            "currency": "EUR",
            "current_funding": 7550.00,
            "funding_threshold": 10000.00,
            "supporter_count": 150
        }
    ],
    "meta": {
        "total": 3,
        "limit": 20,
        "offset": 0
    }
}

Data Formats

Dates

All dates are returned as UNIX timestamps (seconds since 1970-01-01):

{
    "created_timestamp": 1705315800,
    "support_timestamp": 1705402200
}

Convert in JavaScript:

const date = new Date(timestamp * 1000);

Convert in PHP:

$date = new DateTime('@' . $timestamp);

Pagination

Paginated endpoints support the limit and offset parameters:

curl -H "Authorization: Bearer snx_proj_xxx" \
     "https://www.startnext.com/myty/api/crowdfunding/project/my-project/incentives?limit=10&offset=20"

Edge cases — none of these return 400:

Input Behaviour
limit above 100 Silently clamped to 100. Always page until offset + len(data) >= meta.total; never assume your requested page size was honoured — check meta.limit.
limit=0 or limit= (empty) Treated as absent → falls back to the default of 20
offset=0 or offset= (empty) Falls back to 0 (same result)

The response contains a meta object with pagination information. meta.limit is the limit that was actually applied — compare it against what you sent:

{
    "data": [
        ...
    ],
    "meta": {
        "total": 150,
        "limit": 10,
        "offset": 20
    }
}

Paging is not a snapshot

offset counts positions in a result set that keeps changing while you page through it. If entries are added or removed between two of your requests, positions shift: an entry can land in a page you already fetched (you see it twice) or move past the boundary (you miss it). There is no cursor parameter that would avoid this.

It matters most on the lists that move while a project is collecting support — /fundings, /orders, /subscribers, /wall — and rarely on /incentives or /blog.

Deduplicating is not the same on every list. Only some items carry a stable identifier:

List Stable key
/wall, /blog id
/orders no id — use collection_timestamp plus the buyer to recognise a row
/subscribers no id — use subscribed_timestamp plus user
/fundings no id — use support_timestamp plus user

For the three lists without an id, a duplicate row is not reliably distinguishable from two genuinely similar supports in the same second. Plan for that rather than assuming exactness:

Rate Limiting

API requests are limited to ensure system stability. Which limit applies depends on the credential you use, not on the endpoint.

The values below are the platform defaults and may be adjusted per environment. Do not hard-code them — read the current values from the response headers instead (see below).

Project API Key

Limits depend on the plan of the project the key belongs to:

Project plan Per minute Per day
Pro 120 10,000
Premium 300 50,000
Enterprise 600 100,000

A key belonging to a project on any other plan (Basic, Legacy) falls back to the unauthenticated limits below.

User API Key

Credential Per minute Per day
User API Key 60 5,000

These limits are per key and independent of any project plan — a User API Key that reaches projects on the Premium plan is still limited to 60 requests per minute.

Unauthenticated requests

Endpoints that work without a credential are limited per IP address:

Per minute Per day
No credential 30 1,000

Rate Limit Headers

Responses authenticated with a Project or User API Key carry the current limit status:

X-RateLimit-Day-Limit: 10000
X-RateLimit-Day-Remaining: 9850
X-RateLimit-Day-Reset: 1705363200
X-RateLimit-Minute-Limit: 120
X-RateLimit-Minute-Remaining: 118
X-RateLimit-Minute-Reset: 1705320060

Unauthenticated responses use the same six fields with a X-RateLimit-Public- prefix (X-RateLimit-Public-Minute-Limit, X-RateLimit-Public-Day-Remaining, …). The *-Reset fields are UNIX timestamps.

When the Limit Is Exceeded

When the limit is reached, you receive an HTTP 429. The wait time is in the Retry-After header (seconds) — it is not part of the response body:

HTTP/1.1 429 Too Many Requests
Retry-After: 45
{
    "error": "rate_limit_exceeded",
    "error_description": "Rate limit exceeded (minute). Please wait 45 seconds before retrying."
}

Read your backoff from the Retry-After header. error_description names the limit that was exceeded (minute or day), but it is a human-readable message — do not parse it.

Error Handling

HTTP Status Codes

Code Meaning
200 Success
400 Bad request — a value in your request is not valid
401 Invalid or missing API key
403 Missing permission (scope), or the project does not exist
429 Rate limit exceeded
500 Server error

An unknown project answers 403, not 404. The API does not distinguish "this project does not exist" from "this project is not visible to your credential" — a 404 would confirm the existence of a project you are not allowed to see. Both cases return 403 with error: "project_access_denied". Check the project slug first when you get an unexpected 403.

Error Response

All errors use the same envelope. The HTTP status code is only in the status line — there is no status field in the body.

{
    "error": "invalid_api_key",
    "error_description": "The provided User API Key is invalid or has been disabled"
}
{
    "error": "insufficient_scope",
    "error_description": "This endpoint requires the \"fundings:read\" scope"
}

Branch on error — it is a stable, machine-readable code. error_description is a human-readable message that may be null and may change without notice.

Error Codes

The error values you can encounter, grouped by what produces them, are listed on their own page: Error Codes. It is generated from the API specification, so it does not go stale. The interactive API reference additionally shows the possible codes for each individual response.

Versioning and Deprecation

Be aware of what the API does not currently guarantee, so you can plan accordingly:

Practical consequence: fetch the OpenAPI specification periodically and diff it — that is currently the only reliable way to notice that a field, an endpoint or a default has changed. Code defensively: treat unknown response fields as additive rather than failing on them, and do not depend on field order or on the exact wording of error_description.

Retries and Idempotency

There is no Idempotency-Key support. Whether a retry is safe depends on the endpoint:

Operation Safe to retry? What a retry does
All GET endpoints ✅ Yes Read-only, no side effects
POST /user/me/image (user:write) ✅ Yes Replaces the profile image — repeating it yields the same end state
Single-slot image uploads (project title, logo, story) ✅ Yes Replaces the image in that slot
Gallery image uploads (project, reward, blog) No Appends. A retry adds a duplicate image and counts against the limit of 5 per gallery (422 gallery_full)
POST /project (project:write) No Creates another project. There is no deduplication — a retried request after a timeout leaves you with two projects, and it counts against a daily creation limit (default 10, then 429 rate_limit_exceeded)

For the non-idempotent operations: on a timeout or a 5xx, do not blind-retry. Read back the current state first (GET /user/me/projects or the relevant gallery endpoint) and only retry if the object is genuinely missing.

429 is always safe to retry — wait for Retry-After first. 4xx other than 429 will not succeed on retry; fix the request instead.

Code Examples & Usage Examples

Runnable code for calling the API in JavaScript, PHP and Python, plus two complete integrations (a funding-progress widget and a rewards shop backend) have moved to their own page: Examples.