Partner API

A site with a key pulls that brand's public ArchState data, and can post contact-form enquiries. Version lives in the path: /api/partner/v1.

Call

Send the header below on every request. Do not send cookies.

Authorization: Bearer <key>

A personal key and a studio key are different brands. The other brand route is not found.

  • Personal key: GET /api/partner/v1/profile
  • Studio key: GET /api/partner/v1/workspace
  • One project: GET /api/partner/v1/project/{id}, using an id from that brand's projects array
  • Contact form: POST /api/partner/v1/enquiry (same key)

There is no bulk projects route. Production origin is https://archstate.archi.

Profile

The person behind a personal key. The response is { data }.

{
  "data": {
    "id": "profile id",
    "username": "vahid-ghodsi",
    "first_name": "Vahid",
    "last_name": "Ghodsi",
    "job_title": "Architect",
    "company": "Method Architects",
    "description": "…",
    "photo_url": "https://…",
    "location": { "city": "…", "country": "…" },
    "contacts": { "emails": [], "phones": [], "social_media": [], "websites": [] },
    "projects": [{ "id": "…", "title": "…", "headline": "…", "thumbnail_url": "https://…", "workspace_id": "…", "workspace_name": "Method Architects", "workspace_thumbnail_url": "https://…" }]
  }
}

projects lists every public project this person is on, including work in other studios. workspace_name is that project's owner, and workspace_thumbnail_url is the owner image. A row may also include status, categories, types, location, floor_area, site_area, area_unit, date_start, and date_end. Those are absent until that project is saved again. date_start and date_end are the project span. The profile locationis this person's city and country, and is omitted when unset. Contacts are the personal workspace's public emails, phones, websites, and socials.

Workspace

The studio behind a studio key.

{
  "data": {
    "id": "workspace id",
    "type": "studio",
    "name": "Method Architects",
    "handle": "method-architects",
    "description": "…",
    "thumbnail_url": "https://…",
    "location": { "city": "…", "country": "…" },
    "contacts": { "emails": [], "phones": [], "social_media": [], "websites": [] },
    "projects": [{ "id": "…", "title": "…", "headline": "…", "thumbnail_url": "https://…", "workspace_id": "…", "workspace_name": "Method Architects", "workspace_thumbnail_url": "https://…" }]
  }
}

projects lists public projects this studio owns. Each row's workspace_name and workspace_thumbnail_url are this studio. Listing fields match the profile list.

Project

Load this when a page needs more than the list. The body adds description, the same listing fields, milestone dates, contributors, and groups. Each group has sections. Each section has text and assets (url, thumbnail_url, title, alt, format). Use those URLs as returned. Listing fields here come from the live project. dates is the milestone list, not the span.

Enquiry

Post a contact-form submission with the same key. ArchState stores it on that brand's workspace and emails the owner. There is no visitor receipt from this API.

POST /api/partner/v1/enquiry
Content-Type: application/json

{
  "name": "Alex Rivera",
  "email": "alex@example.com",
  "message": "…",
  "subject": "optional",
  "hp": ""
}

Success is 201 with { data: { id } }. Include a hidden empty hp field as a honeypot. Name, email, and message are required. Submissions are rate-limited per key.

Browser access

A browser sends Origin. The hostname must be on the key's allowlist. The port is ignored, so localhost covers port 3000. A request with no Origin, such as curl, is allowed when the key matches. Preflight is OPTIONS on the same path and does not need the key.

Errors

Error JSON is { message, code, status }.

  • 401 INVALID_PARTNER_API_KEY — missing or unknown key
  • 403 PARTNER_ORIGIN_NOT_ALLOWED — this site host is not allowlisted
  • 404 NOT_FOUND — this key cannot use that brand route
  • 404 PROJECT_NOT_FOUND— that project is not on this key's public list
  • 400 MISSING_REQUIRED_DATA — missing required fields or project URL with no id
  • 429 RATE_LIMIT_EXCEEDED — too many enquiries; wait and retry later

Private and deleted projects use the same not-found response as an unknown id.

Agent instruction

Paste this into the coding agent on the site that holds the key. Tell it whether the key is personal or studio.

You are wiring a website to the ArchState Partner API v1.

Base URL: the ArchState origin. Production is https://archstate.archi. Use http://localhost:8080 only when the task says this is the development environment.
Key: environment variable ARCHSTATE_PARTNER_KEY. Never print, log, or commit the key.
Brand: this key is either personal or studio. Ask if that is not already stated. Do not call both brand routes.

Request: Authorization: Bearer <key>. Do not send cookies. Do not send Firebase Auth or App Check.

Personal key:
- GET /api/partner/v1/profile
- Then GET /api/partner/v1/project/{id} only for ids in data.projects

Studio key:
- GET /api/partner/v1/workspace
- Then GET /api/partner/v1/project/{id} only for ids in data.projects

Either key:
- POST /api/partner/v1/enquiry with JSON { name, email, message, subject? }. Optional honeypot field hp must be empty or omitted.
- Success is 201 { data: { id } }. Do not email the visitor from this API; ArchState notifies the owner.

The first GET response is the public brand plus a project list. Each row has id, title, headline, thumbnail_url, and the owning workspace (workspace_id, workspace_name, workspace_thumbnail_url). workspace_name is the owner label. workspace_thumbnail_url is the owner image. Those workspace fields may be absent on older rows. A row may also include status, categories, types, location ({ city, country }), floor_area, site_area, area_unit ("m2" or "ft2"), date_start, and date_end. Those listing fields are absent until that project is saved again. date_start and date_end are the project span. Use the list to render and filter. Load a project when a page needs the description, milestone dates, contributors, or section assets.

A personal profile may include location ({ city, country }). It is omitted when unset. A personal list is every public project that person is on, including work in other studios. Group or label those by workspace_id and workspace_name. A studio list is the public projects that studio owns; each row's workspace is that studio.

Success body is { data }. Errors are { message, code, status }.
- 401 INVALID_PARTNER_API_KEY: stop. The key is missing or unknown.
- 403 PARTNER_ORIGIN_NOT_ALLOWED: stop. The site hostname is not allowlisted. The port does not matter.
- 404 NOT_FOUND: this key cannot use that brand route.
- 404 PROJECT_NOT_FOUND: that project is not on this key's public list.
- 400 MISSING_REQUIRED_DATA: missing required fields or project URL with no id.
- 429 RATE_LIMIT_EXCEEDED: too many enquiries; wait and retry later.
Do not retry 401, 403, or 404.

Do not invent /api/partner/v1/projects or a second brand on this key.
Use image and file URLs exactly as returned.

Contract: the TypeScript below is the public shape. A field marked optional may be absent. Empty strings are omitted. Do not add fields.

type PartnerApiSuccess<T> = { data: T };
type PartnerApiError = { message: string; code: string; status: number };

type PartnerPublicContacts = {
  emails: Array<{ email: string; title?: string }>;
  phones: Array<{ country_code: string; phone: string; title?: string }>;
  social_media: Array<{
    platform: 'linkedin' | 'x' | 'instagram' | 'facebook' | 'youtube';
    username?: string;
    url: string;
  }>;
  websites: Array<{ title?: string; url: string }>;
};

type PartnerProjectSummary = {
  id: string;
  title: string;
  headline?: string;
  thumbnail_url?: string;
  workspace_id?: string;
  workspace_name?: string;
  workspace_thumbnail_url?: string;
  status?: string;
  categories?: string[];
  types?: string[];
  location?: { city?: string; country?: string };
  floor_area?: number;
  site_area?: number;
  area_unit?: 'm2' | 'ft2';
  date_start?: string;
  date_end?: string;
};

type PartnerProfile = {
  id: string;
  username: string;
  first_name: string;
  last_name: string;
  job_title?: string;
  company?: string;
  description?: string;
  photo_url?: string;
  location?: { city?: string; country?: string };
  contacts?: PartnerPublicContacts;
  projects: PartnerProjectSummary[];
};

type PartnerWorkspace = {
  id: string;
  type: 'personal' | 'studio';
  name: string;
  handle?: string;
  description?: string;
  thumbnail_url?: string;
  location?: { city?: string; country?: string };
  contacts?: PartnerPublicContacts;
  projects: PartnerProjectSummary[];
};

type PartnerAsset = {
  id: string;
  title?: string;
  alt?: string;
  description?: string;
  url?: string;
  thumbnail_url?: string;
  file_format?: string;
};

type PartnerContributor = {
  id: string;
  first_name: string;
  last_name: string;
  photo_url?: string;
  team_role?: string;
  team_section?: string;
};

type PartnerProjectDate = {
  id: string;
  title: string;
  type: string;
  date: string;
  is_primary?: boolean;
};

type PartnerProjectSection = {
  id: string;
  title: string;
  template_type: string;
  descriptions?: Array<{ title?: string; text: string }>;
  assets: PartnerAsset[];
};

type PartnerProjectGroup = {
  title: string;
  sections: PartnerProjectSection[];
};

type PartnerProject = {
  id: string;
  title: string;
  headline?: string;
  description?: string;
  thumbnail_url?: string;
  status?: string;
  categories?: string[];
  types?: string[];
  location?: { city?: string; country?: string };
  floor_area?: number;
  site_area?: number;
  area_unit?: 'm2' | 'ft2';
  date_start?: string;
  date_end?: string;
  dates?: PartnerProjectDate[];
  contributors: PartnerContributor[];
  groups: PartnerProjectGroup[];
};

type PartnerEnquiryBody = {
  name: string;
  email: string;
  message: string;
  subject?: string;
  hp?: string;
};

GET /api/partner/v1/profile → PartnerApiSuccess<PartnerProfile>
GET /api/partner/v1/workspace → PartnerApiSuccess<PartnerWorkspace>
GET /api/partner/v1/project/{id} → PartnerApiSuccess<PartnerProject>
POST /api/partner/v1/enquiry body → PartnerEnquiryBody
POST /api/partner/v1/enquiry success → PartnerApiSuccess<{ id: string }>
Errors → PartnerApiError