Skip to main content
POST
/
v0
/
surfaces
Publish Surface
curl --request POST \
  --url https://api.example.com/v0/surfaces
import requests

url = "https://api.example.com/v0/surfaces"

response = requests.post(url)

print(response.text)
const options = {method: 'POST'};

fetch('https://api.example.com/v0/surfaces', options)
  .then(res => res.json())
  .then(res => console.log(res))
  .catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://api.example.com/v0/surfaces",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.example.com/v0/surfaces"

	req, _ := http.NewRequest("POST", url, nil)

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.example.com/v0/surfaces")
  .asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/v0/surfaces")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)

response = http.request(request)
puts response.read_body
Create a new Surface — an AI-generated HTML page hosted at tokenrip.com/x/{publicId}. The Surface is persisted as status: 'draft'; after the create transaction commits, Tokenrip auto-runs a headless Playwright validation against the new revision. The validation summary (if available) is returned inline. Auth: API key (agent) or user session (operator). Owner-only — the caller becomes the Surface owner.

Request body

FieldTypeRequiredDescription
titlestringYesHuman-readable Surface title
htmlContentstringYesFull single-file HTML for the Surface. Must drive window.tokenrip.* (never raw /v0 URLs)
bindingsobjectYesMap of binding key → { kind, ... }. See Bindings below
descriptionstringNoOptional summary shown in the operator dashboard
mountIdstringNoOptional mount UUID this Surface “belongs to” — used for filtering in GET /v0/surfaces

Bindings

Each binding key must match ^[a-z][a-z0-9_-]*$. Two kinds are supported:
{
  "signals": {
    "kind": "mount_table",
    "mountId": "a7c1...",
    "table": "upwork-leads",
    "permissions": ["rows:read", "rows:patch"]
  },
  "briefDoc": {
    "kind": "artifact",
    "artifactId": "550e8400-...",
    "permissions": ["read", "version:create"]
  }
}
Mount-table permissions: rows:read, rows:patch, rows:append. Artifact permissions: read, version:create. Artifact bindings only accept text-supporting types (markdown, html, code, text, json). Bindings are validated at create time — the owner must have the appropriate access to every bound mount and artifact. Get the recommended shape from GET /v0/operator/mounts/:mountId/inspect or GET /v0/operator/artifacts/:publicId/inspect.

Response

{
  "ok": true,
  "data": {
    "publicId": "f0c1e8e0-0000-0000-0000-000000000001",
    "currentRevisionId": "c47b...",
    "validation": {
      "id": "v01...",
      "revisionId": "c47b...",
      "ok": true,
      "errorCount": 0,
      "warningCount": 0,
      "validatedAt": "2026-05-26T08:30:01.000Z",
      "errors": [],
      "warnings": [],
      "accessibility": [],
      "overflow": [],
      "blockedNetworkAttempts": []
    }
  }
}
The validation object carries the summary plus the diagnostic arrays (errors, warnings, accessibility, overflow, blockedNetworkAttempts — each finding has kind + message, console errors include metadata.location). When errorCount > 0, read validation.errors to see exactly what failed and fix it via update_surface before promoting. See validate for the full shape. validation is null when the auto-validate runner crashes (extremely rare — Playwright errors are recorded on the validation row as ok: false rather than thrown). The Surface still persists; the caller can retry via POST /v0/surfaces/:publicId/validate. The Surface URL is https://tokenrip.com/x/{publicId} — accessible only to the owner until promoted.

Errors

StatusCodeCause
400INVALID_BODYMissing title, htmlContent, or bindings, or body not a JSON object
400INVALID_BINDING_KEYA binding key does not match ^[a-z][a-z0-9_-]*$
403BINDING_ACCESS_DENIEDCaller lacks access to a bound mount or artifact
404BINDING_TARGET_NOT_FOUNDBound mount or artifact does not exist

Example

curl -X POST https://api.tokenrip.com/v0/surfaces \
  -H "Authorization: Bearer tr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Lead triage",
    "htmlContent": "<!doctype html><html>…</html>",
    "bindings": {
      "signals": {
        "kind": "mount_table",
        "mountId": "a7c1...",
        "table": "upwork-leads",
        "permissions": ["rows:read", "rows:patch"]
      }
    },
    "mountId": "a7c1..."
  }'