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:
- Open your project on Startnext
- Go to Project Interface → Administration → API Keys
- Click Create New API Key
- Select the required permissions (scopes)
- 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:
- Log in to Startnext
- Go to Settings → API & Integrations
- Click Create New API Key
- Enter a name — the
user:readscope is always included automatically - 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:
- HTML source
- JavaScript that is delivered to the browser
- a Git repository
- publicly readable configuration files
- e-mails or support tickets
A browser request that works is not a request that is safe. Key-authenticated responses carry
Access-Control-Allow-Origin: *, so afetch()with anAuthorizationheader 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
incentivethroughout 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
startedonward, 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 withproject:read, or a User API Key belonging to the project's Owner, Manager or a Team member, additionally unlocks projects in statusnew(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
statusfield is legacy. It is a hard-coded0on every successful response and carries no information — do not branch on it. Use the HTTP status code instead. Note that it is unrelated toproject.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:
- A reward may have no image at all. Your layout has to work without one — do not render an
imgwith an emptysrc. - The image host is not guaranteed to stay the same. Thumbnail URLs are absolute and are served from the platform's own delivery domain, which can differ per environment. Use the URL as returned; do not reject it based on a hard-coded hostname, and do not rebuild it from the parts.
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_onlyare always part of the list and ofmeta.total— only their content is gated. Without authorization they come back redacted:is_lockedistrue,descriptionholds a short public teaser (max. 400 characters) andimage,image_mobile,gallery_images,embed_url_1…3andvideoare empty. The full content is returned when the request setsinclude_supporter_only=1and 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 withinclude_supporter_only=1bypass the CDN cache.
reading_time_minutesis 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, whereasuserobjects on other endpoints usefirst_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
/ordersendpoint with theorders:readscope 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.jsonoperation 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:
- Keep the paging window short. The less time between your first and last request, the fewer changes fall into it. For a nightly export, one quick pass beats a slow one.
- Treat
meta.totalas a moving number. It can differ between the first and the last request of the same run — do not use it to assert that you fetched everything. - For a count, read
meta.totalinstead of counting collected rows: it is computed server-side in one query and cannot drift across your requests. - If you need exactness, re-read the whole list and replace your copy, rather than merging pages into an existing one.
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, not404. The API does not distinguish "this project does not exist" from "this project is not visible to your credential" — a404would confirm the existence of a project you are not allowed to see. Both cases return403witherror: "project_access_denied". Check the project slug first when you get an unexpected403.
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:
- No version in the URL or in a header. The base URL carries no version segment and there is no
Accept-VersionorX-API-Versionheader. There is exactly one live version of every endpoint. info.versionin the OpenAPI spec is not a contract. It reads1.0.0and is not incremented per change — do not pin against it.- Deprecations are marked in the spec, not announced by a header. An operation being retired
carries
deprecated: truein the OpenAPI document. There is noSunsetorDeprecationresponse header and no committed notice period. - No changelog endpoint. The only machine-readable source of truth is the spec itself.
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.