Public API Documentation
Integrate Stone Project with your systems using the Public API. Create tokens in Settings β API Settings to get started.
/docs/openapi.json (import it in Postman, Insomnia or a
code generator) and the full reference is at /docs/reference.
Connection test: GET /me returns the company and the scopes of the token.
Authentication
All requests must include a valid API token in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
API tokens are created in the app at Settings β API Settings. Each token has specific scopes that control which resources can be accessed.
Base URL
https://api.stoneproject.app/api/v1/public
All endpoint paths below are relative to this base URL.
Response Format
All responses follow a consistent JSON format:
{
"success": true,
"data": { ... },
"message": "Optional message"
}
Pagination
List endpoints return paginated results. Use per_page (max 100) and page query
parameters.
{
"success": true,
"data": {
"items": [ ... ],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 25,
"total": 60
}
}
}
Rate Limiting
The API is rate-limited. The current limits are returned in response headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per window |
X-RateLimit-Remaining |
Requests remaining in current window |
Retry-After |
Seconds until rate limit resets (when exceeded) |
Error Handling
Errors return a success: false response with an error code:
{
"success": false,
"message": "Token is invalid or revoked",
"error_code": "token_invalid"
}
| HTTP Code | Error Code | Description |
|---|---|---|
| 401 | token_missing |
No Authorization header provided |
| 401 | token_invalid |
Token not found or revoked |
| 401 | token_revoked |
Token has been deactivated |
| 401 | token_expired |
Token has expired |
| 403 | scope_required |
Token lacks the required scope |
| 404 | β | Resource not found |
| 422 | β | Validation error (check errors field) |
| 429 | β | Rate limit exceeded |
Projects
GET /projects projects.read
List all projects for your company.
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status |
stage |
string | Filter by kanban stage |
client_id |
uuid | Filter by client |
assigned_to |
uuid | Filter by assigned team member |
search |
string | Search in title and description |
created_after |
date | Filter by creation date (after) |
created_before |
date | Filter by creation date (before) |
with_client |
boolean | Include client data |
per_page |
integer | Results per page (max 100) |
GET /projects/{id} projects.read
Get a single project by ID. Client data is included automatically.
POST /projects projects.write
Create a new project.
| Field | Type | Description | |
|---|---|---|---|
title |
string | required | Project title |
description |
string | optional | Project description |
client_id |
uuid | optional | Associated client |
stage |
string | optional | Kanban stage (default: "new") |
priority |
string | optional | low, normal, high, urgent |
source |
string | optional | Lead source |
estimated_value |
numeric | optional | Estimated value |
assigned_to |
uuid | optional | Assigned team member |
tags |
array | optional | List of tags |
deadline |
date | optional | Project deadline |
address |
string | optional | Street address |
city |
string | optional | City |
state |
string | optional | State / Province |
zip_code |
string | optional | ZIP / Postal code |
contact_name |
string | optional | On-site contact name |
contact_phone |
string | optional | On-site contact phone |
address_line_2 |
string | optional | Address line 2 (suite, unit, etc.) |
country |
string | optional | Country |
scheduled_measurement_at |
datetime | optional | Scheduled measurement date/time (ISO 8601) |
scheduled_installation_at |
datetime | optional | Scheduled installation date/time (ISO 8601) |
purchase_order |
string | optional | Customer purchase order reference |
curl -X POST https://api.stoneproject.app/api/v1/public/projects \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Kitchen Remodel - Smith",
"client_id": "019c...",
"priority": "high",
"estimated_value": 8500
}'
PUT /projects/{id} projects.write
Update an existing project. Same fields as POST /projects above, all optional β send only
what you want to change. Note the method is PUT, not POST: a POST to
/projects creates a new project instead of updating one.
The job site street is address. We also accept address_line_1 as an alias for
it, since the field beside it is called address_line_2. Any key we do not recognise is
ignored and named back to you in the response message, so a typo never passes as a silent
success.
curl -X PUT https://api.stoneproject.app/api/v1/public/projects/019c... \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"address": "4501 Cox Rd",
"address_line_2": "Suite 200",
"city": "Glen Allen",
"state": "VA",
"zip_code": "23060",
"contact_name": "Faro Vitale",
"contact_phone": "+1 718 208 3214"
}'
Response Example
A project object includes:
{
"success": true,
"data": {
"id": "019c...",
"title": "Kitchen Remodel - Smith",
"status": "active",
"stage": "quoted",
"priority": "high",
"estimated_value": 8500.00,
"deadline": "2026-05-15",
"client": {
"id": "019c...",
"name": "John Smith"
},
"created_at": "2026-03-10T14:30:00Z",
"updated_at": "2026-04-01T09:15:00Z"
}
}
Clients
GET /clients clients.read
List all clients for your company.
| Parameter | Type | Description |
|---|---|---|
search |
string | Search by name, email, phone, or account |
source |
string | Filter by lead source |
created_after |
date | Filter by creation date (after) |
created_before |
date | Filter by creation date (before) |
per_page |
integer | Results per page (max 100) |
GET /clients/{id} clients.read
Get a single client by ID. Includes project count.
POST /clients clients.write
Create a new client.
| Field | Type | Description | |
|---|---|---|---|
name |
string | required | Client name |
email |
string | optional | Email address |
phone |
string | optional | Primary phone |
cell |
string | optional | Cell phone |
account_name |
string | optional | Company / Account name |
address |
string | optional | Street address |
city |
string | optional | City |
state |
string | optional | State |
zip_code |
string | optional | ZIP code |
notes |
string | optional | Internal notes |
source |
string | optional | Lead source |
tags |
array | optional | List of tags |
external_id |
string | optional | Your external reference ID |
curl -X POST https://api.stoneproject.app/api/v1/public/clients \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "John Smith",
"email": "[email protected]",
"phone": "555-0100",
"source": "website"
}'
PUT /clients/{id} clients.write
Update an existing client. All fields are optional.
Response Example
A client object includes:
{
"success": true,
"data": {
"id": "019c...",
"name": "John Smith",
"email": "[email protected]",
"phone": "555-0100",
"account_name": "Smith Design Co.",
"source": "website",
"projects_count": 3,
"created_at": "2026-01-20T10:00:00Z"
}
}
Quotes
GET /quotes quotes.read
List all quotes for your company.
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status (draft, sent, accepted, rejected, expired) |
project_id |
uuid | Filter by project |
client_id |
uuid | Filter by client |
created_after |
date | Filter by creation date (after) |
created_before |
date | Filter by creation date (before) |
with_client |
boolean | Include client data |
with_project |
boolean | Include project data |
per_page |
integer | Results per page (max 100) |
GET /quotes/{id} quotes.read
Get a single quote by ID. Client and project data are included automatically.
Orders
Closed sales orders β read-only access to finalized deals. An order is created when a quote is accepted and the sale is closed.
GET /orders orders.read
List all orders for your company.
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status (active, cancelled, completed) |
payment_status |
string | Filter by payment status (unpaid, partial, paid) |
project_id |
uuid | Filter by project |
quote_id |
uuid | Filter by the originating quote |
sale_date_from |
date | Filter by sale date (on or after, YYYY-MM-DD) |
sale_date_to |
date | Filter by sale date (on or before, YYYY-MM-DD) |
created_after |
date | Filter by creation date (after) |
created_before |
date | Filter by creation date (before) |
per_page |
integer | Results per page (max 100) |
Response Example
{
"success": true,
"data": {
"items": [
{
"id": "019e-...",
"number": "ORD-2026-00042",
"name": "Kitchen Renovation β Smith",
"status": "active",
"payment_status": "partial",
"currency": "USD",
"subtotal": 7200.00,
"tax": 504.00,
"discount_amount": 0.00,
"total": 7704.00,
"amount_paid": 3852.00,
"balance_due": 3852.00,
"sale_date": "2026-06-15",
"payment_due_date": "2026-07-15",
"payment_terms": "net30",
"payment_terms_days": 30,
"deposit_required": true,
"deposit_percentage": 50,
"deposit_amount": 3852.00,
"installments_count": 2,
"sqft": 48.5,
"po_number": "PO-98765",
"salesperson": "Jane Doe",
"notes": null,
"project_id": "019c-...",
"quote_id": "019d-...",
"lead_name": "John Smith",
"account_name": "Smith Family",
"created_at": "2026-06-15T10:00:00Z",
"updated_at": "2026-06-20T14:00:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 10,
"total": 28
}
}
}
GET /orders/{id} orders.read
Get a single order by ID. The line_items array is included only on this detail response (not on the list). Internal cost, commission, and margin keys are stripped from each line item.
Invoices
GET /invoices invoices.read
List all invoices for your company.
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status (draft, sent, paid, overdue, cancelled) |
project_id |
uuid | Filter by project |
client_id |
uuid | Filter by client |
created_after |
date | Filter by creation date (after) |
created_before |
date | Filter by creation date (before) |
due_after |
date | Filter by due date (after) |
due_before |
date | Filter by due date (before) |
with_client |
boolean | Include client data |
with_project |
boolean | Include project data |
with_payments |
boolean | Include payments list |
per_page |
integer | Results per page (max 100) |
GET /invoices/{id} invoices.read
Get a single invoice by ID. Client, project, and payments are included automatically.
Payments
GET /payments payments.read
List all payments for your company.
| Parameter | Type | Description |
|---|---|---|
invoice_id |
uuid | Filter by invoice |
status |
string | Filter by status (pending, completed, failed, refunded) |
payment_method |
string | Filter by payment method |
created_after |
date | Filter by creation date (after) |
created_before |
date | Filter by creation date (before) |
with_invoice |
boolean | Include invoice data |
per_page |
integer | Results per page (max 100) |
GET /payments/{id} payments.read
Get a single payment by ID. Invoice summary is included automatically.
Pieces / Inventory
Manage your company's stone pieces and remnants β the core of Stone Project.
List Pieces
List your company's pieces with optional filters. Requires the pieces.read scope. To browse
public remnants across the whole marketplace (other companies' listings), use the
Search endpoint instead.
| Parameter | Type | Description |
|---|---|---|
filter[stone_type_id] |
uuid | Filter by stone type |
filter[material_type_id] |
uuid | Filter by material type |
filter[category_id] |
uuid | Filter by category |
filter[status] |
string | Filter by status: available, reserved, sold |
filter[color] |
string | Filter by color name |
filter[finish] |
string | Filter by finish: polished, honed, leathered,
brushed, flamed
|
sort |
string | Sort by: price, created_at, views_count,
length_cm, width_cm. Prefix with - for descending
|
latitude |
number | Filter by proximity (requires longitude and radius) |
longitude |
number | Filter by proximity |
radius |
number | Radius in miles for proximity search |
page |
integer | Page number (default: 1, 20 items per page) |
Response Example
{
"data": [
{
"id": "019d-...",
"title": "White Carrara Remnant",
"description": "Beautiful remnant piece, polished finish",
"status": "available",
"dimensions": {
"length_cm": 120,
"width_cm": 60,
"thickness_cm": 3,
"area_sqft": 7.75
},
"price": 350.00,
"price_type": "fixed",
"color": "White",
"finish": "polished",
"origin": "Italy",
"stone_type": { "id": "...", "name": "Marble" },
"category": { "id": "...", "name": "Remnants" },
"images": [
{ "id": 1, "url": "https://...", "thumbnail_url": "https://..." }
],
"company": { "id": "...", "name": "Stone Works LLC" },
"views_count": 42,
"created_at": "2026-03-15T10:30:00Z"
}
],
"meta": { "current_page": 1, "last_page": 5, "per_page": 20, "total": 98 }
}
Get Piece
Get a single piece by ID. Includes all details, images, company info, and related stone type/category.
Create Piece
Create a new piece in your inventory. If coordinates are not provided, the company's location is used.
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Piece title (max 255 chars) |
length_cm |
number | Yes | Length in centimeters (1β99999) |
width_cm |
number | Yes | Width in centimeters (1β99999) |
thickness_cm |
number | No | Thickness in centimeters (0.1β999) |
description |
string | No | Description (max 5000 chars) |
stone_type_id |
uuid | No | Stone type ID (marble, granite, etc.) |
material_type_id |
uuid | No | Material type ID |
category_id |
uuid | No | Category ID |
price |
number | No | Price (0β9999999) |
price_type |
string | No | fixed, negotiable, or on_request |
color |
string | No | Color name (max 100 chars) |
finish |
string | No | polished, honed, leathered, brushed,
flamed
|
origin |
string | No | Origin / country (max 100 chars) |
shape_type |
string | No | rectangle, lShape, uShape, triangle,
irregular, custom
|
Update Piece
Update an existing piece. All fields are optional β only send the fields you want to change.
Delete Piece
Soft-delete a piece. It can be restored later via the app.
Upload Piece Images
Upload images to a piece. Send as multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
images[] |
file | Yes | Image files (JPEG, PNG, WebP). Max 10MB each. |
Update Piece Status
Change the status of a piece.
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | Yes | available, reserved, or sold |
Suppliers
Manage your stone and material suppliers.
List Suppliers
List all suppliers for your company.
| Parameter | Type | Description |
|---|---|---|
filter[name] |
string | Search by supplier name |
filter[is_active] |
boolean | Filter active/inactive suppliers |
filter[city] |
string | Filter by city |
filter[state] |
string | Filter by state |
sort |
string | Sort by: name, created_at, city, country
|
Response Example
{
"success": true,
"data": [
{
"id": "019d-...",
"name": "Premium Stone Supply",
"email": "[email protected]",
"phone": "+1 555-0123",
"city": "Miami",
"state": "FL",
"country": "US",
"is_active": true,
"created_at": "2026-01-10T08:00:00Z"
}
]
}
Get Supplier
Get a single supplier by ID.
Create Supplier
Create a new supplier.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Supplier name (max 255) |
email |
string | No | Contact email |
phone |
string | No | Phone number (max 20) |
address |
string | No | Street address |
city |
string | No | City |
state |
string | No | State / Province |
country |
string | No | Country |
postal_code |
string | No | Postal code (max 20) |
website |
string | No | Website URL |
tax_id |
string | No | Tax ID / EIN (max 30) |
notes |
string | No | Internal notes (max 2000) |
Update Supplier
Update an existing supplier. All fields are optional.
Delete Supplier
Delete a supplier.
Tasks
List tasks, newest first. Filters: project_id, status
(pending, in_progress, completed, dismissed, snoozed).
Create a task. title is required; optional description, project_id
(a project of this company), assigned_to (an active member; defaults to the owner),
priority (low, medium, high, urgent) and due_date (defaults to today).
Leads
Create a lead: the client (matched by email, then phone, so the same person stays one client) and a project
in the lead stage, exactly like a website form does. name and email or
phone are required; optional message, address fields and
utm_source/utm_medium/utm_campaign. The same lead sent twice within
10 minutes returns the first one with deduplicated: true.
Time entries
Clock punches, newest first: who, check-in and check-out, break and total minutes, review status. Filters:
user_id, from, to (dates). GPS and photos are never exposed.
Webhook subscriptions
REST hooks for Zapier, Make and your own code: subscribe a URL when your automation turns on, unsubscribe when it turns off. Deliveries are the same signed, retried webhooks described below.
Body: url (public https), events (array of event names, or ["*"]),
optional name. The response carries the signing secret once.
Search
Search the Remnant Finder marketplace for pieces across all companies.
Search Pieces
Full-text search across public pieces. Supports filters, geo search, and sorting.
| Parameter | Type | Description |
|---|---|---|
q |
string | Free text search query |
stone_type_id |
uuid | Filter by stone type |
material_type_id |
uuid | Filter by material type |
category_id |
uuid | Filter by category |
color |
string | Filter by color name |
finish |
string | Filter by finish type |
min_price |
number | Minimum price filter |
max_price |
number | Maximum price filter |
min_length |
number | Minimum length in cm |
min_width |
number | Minimum width in cm |
latitude |
number | Center point latitude for geo search |
longitude |
number | Center point longitude for geo search |
radius |
number | Search radius in miles |
sort |
string | price, -price, created_at, -created_at,
distance
|
Stone Types
Reference data for stone types (Marble, Granite, Quartz, etc.). Read-only.
List Stone Types
Returns all available stone types. Requires a valid API token (Bearer); no specific scope needed. Each item also includes slug and icon.
Response Example
{
"data": [
{ "id": "019a-...", "name": "Marble" },
{ "id": "019a-...", "name": "Granite" },
{ "id": "019a-...", "name": "Quartz" },
{ "id": "019a-...", "name": "Quartzite" },
{ "id": "019a-...", "name": "Porcelain" }
]
}
Categories
Piece categories (Slabs, Remnants, Countertops, etc.). Read-only.
List Categories
Returns all active categories, ordered by display order. Requires a valid API token (Bearer); no specific scope needed. Each item includes id, name, slug, icon.
Colors
System and custom colors used for pieces.
List Colors
Returns system colors plus your company's custom colors.
| Parameter | Type | Description |
|---|---|---|
search |
string | Search by color name |
Create Color
Create a custom color for your company.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Color name (max 100) |
hex_code |
string | No | Hex code (e.g. #FFFFFF) |
Origins
Stone origin countries and regions.
List Origins
Returns system origins plus your company's custom origins.
| Parameter | Type | Description |
|---|---|---|
search |
string | Search by origin name |
Create Origin
Create a custom origin for your company.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Origin name (max 100) |
country_code |
string | No | ISO 2-letter country code |
flag_emoji |
string | No | Flag emoji (max 10) |
Available Scopes
Each API token is created with specific scopes that control access:
| Scope | Description |
|---|---|
projects.read |
View projects |
projects.write |
Create and update projects |
clients.read |
View clients |
clients.write |
Create and update clients |
quotes.read |
View quotes |
invoices.read |
View invoices |
payments.read |
View payments |
orders.read |
View orders |
pieces.read |
View pieces and inventory |
pieces.write |
Create, update, and delete pieces |
suppliers.read |
View suppliers |
suppliers.write |
Create, update, and delete suppliers |
colors.write |
Create custom colors |
origins.write |
Create custom origins |
Read-only reference data (stone types, categories, colors, origins) and the marketplace Search require only a valid token β no specific scope.
Webhooks
Webhooks allow you to receive real-time notifications when events occur in your account. Configure webhook URLs in Settings β API Settings β Webhooks.
Subscribe one or more HTTPS endpoints to the events your integration cares about. Each delivery is signed (HMAC-SHA256) so you can verify it came from us β see Verifying the signature below.
Available Events
| Event | Description |
|---|---|
quote.created |
A new quote was created |
quote.sent |
A quote was sent to the client |
quote.accepted |
A quote was accepted |
quote.rejected |
A quote was rejected |
quote.expired |
A quote expired |
quote.deposit_paid |
A quote deposit was paid |
project.created |
A new project was created. Besides data.client_id, the payload embeds the whole
client under data.client (name, account name, email, phone, city/state/country,
source) so you can create the contact in your CRM without a second call. It is
null only when the project has no client attached. |
project.stage_changed |
A project moved to a new stage (the payload's previous holds the prior stage). Use the
data.stage value to react to production, installation, completion, etc. |
client.created |
A new client was added |
client.updated |
A client's public details changed |
invoice.paid |
An invoice was marked as paid |
payment.received |
A payment was recorded |
order.created |
A new order was created (sale closed) |
order.status_changed |
An order changed status. The payload includes "previous": { "status": "active" } with the prior value. |
time_entry.clocked_in |
A crew member clocked in |
time_entry.clocked_out |
A crew member clocked out (the payload carries the total minutes) |
Subscribe to * to receive every event.
Webhook Payload
Every delivery is a JSON POST with a stable envelope. data is the public
representation of the entity (the same shape as the matching GET endpoint). previous
is present only on change events and holds the prior values of the changed fields.
{
"id": "evt_01J9Z4Q1ABC...",
"event": "quote.accepted",
"api_version": "v1",
"created_at": "2026-06-26T14:30:00+00:00",
"company_id": "019c...",
"data": {
"id": "019d...",
"number": "Q-2026-00012",
"project_id": "019c...",
"status": "accepted",
"total": 8500.00,
"accepted_at": "2026-06-26T14:30:00+00:00"
},
"previous": { "status": "sent" }
}
The id (evt_β¦) is unique per event β use it to make your handler idempotent, since a
delivery may be retried. Delivery order is best-effort, not guaranteed.
Verifying the signature
Each request carries these headers:
| Header | Value |
|---|---|
X-RemnantFinder-Signature |
t=<unix_timestamp>,v1=<hex_hmac> |
X-RemnantFinder-Event |
The event name (e.g. quote.accepted) |
X-RemnantFinder-Delivery |
Unique id of this delivery attempt |
The signature is HMAC-SHA256 of the string "{t}.{raw_request_body}", keyed with your
endpoint's signing secret (shown only once, when you create or rotate the endpoint).
Verify it against the raw request body (before any JSON parsing), and reject timestamps
outside a tolerance window (e.g. 5 minutes) to prevent replay attacks.
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_REMNANTFINDER_SIGNATURE']; // "t=...,v1=..."
parse_str(strtr($header, ',', '&'), $p);
$expected = hash_hmac('sha256', $p['t'] . '.' . $payload, $YOUR_SIGNING_SECRET);
if (! hash_equals($expected, $p['v1']) || abs(time() - (int) $p['t']) > 300) {
http_response_code(400);
exit; // reject: bad signature or stale timestamp
}
// trusted β respond 2xx quickly to acknowledge receipt
Delivery & retries
- Only HTTPS endpoints are accepted, and the target must resolve to a public address.
- A delivery is successful on any
2xxresponse. Respond within a few seconds. - Failed deliveries are retried up to 5 times with exponential backoff (~1m β 5m β 30m β 2h β 5h).
- After repeated consecutive failures the endpoint is automatically disabled and the account owner is notified. Re-enable it from Settings β API Settings β Webhooks, where you can also browse the delivery log and replay individual deliveries.
- Send test posts a delivery whose
eventispingand whosedatais a real sample β your most recent record for the first event the endpoint subscribes to, serialized exactly like the live payload. Point your automation platform's field mapping at that request instead of creating a throwaway record just to get an example.