Zomer Gregorio Portfolio API
The Portfolio API gives agents and developers deterministic, structured access to Zomer Gregorio's published professional information without scraping HTML.
Endpoints
| Service | URL |
|---|---|
| REST API index | https://zomeru.dev/api/v1 (opens in a new tab) |
| OpenAPI 3.2 | https://zomeru.dev/openapi.json (opens in a new tab) |
| Portfolio MCP | https://zomeru.dev/api/mcp (opens in a new tab) |
| Documentation MCP | https://zomeru.dev/api/mcp/docs (opens in a new tab) |
| API catalog | https://zomeru.dev/.well-known/api-catalog (opens in a new tab) |
| Portfolio MCP server card | https://zomeru.dev/.well-known/mcp/server-card.json (opens in a new tab) |
| Documentation MCP server card | https://zomeru.dev/.well-known/mcp/docs-server-card.json (opens in a new tab) |
| Agent capabilities | https://zomeru.dev/.well-known/agent-skills/index.json (opens in a new tab) |
The REST contract is version 1.1.0. Breaking REST changes will use a new URL version such as /api/v2; non-breaking additions may remain in /api/v1.
REST resources
| Resource | Method | Endpoint |
|---|---|---|
| API index | GET | /api/v1 |
| Profile | GET | /api/v1/profile |
| Resume | GET | /api/v1/resume |
| Experience | GET | /api/v1/experience |
| Projects | GET | /api/v1/projects |
| Blogs | GET | /api/v1/blogs?limit=10&offset=0 |
| Blog | GET | /api/v1/blogs/{slug} |
| Tech stack | GET | /api/v1/tech-stack |
All versioned portfolio REST resources support anonymous cross-origin reads and return cacheable JSON sourced from published Sanity content. The same-origin notification endpoints in OpenAPI are opt-in mutations and always return Cache-Control: no-store.
REST examples
curl https://zomeru.dev/api/v1
curl https://zomeru.dev/api/v1/profile
curl 'https://zomeru.dev/api/v1/blogs?limit=5&offset=0'
curl https://zomeru.dev/api/v1/resumeMCP
Connect a Streamable HTTP MCP client to:
https://zomeru.dev/api/mcpAvailable portfolio tools:
get_profile— Get Zomer Gregorio's canonical professional profile and public links.get_resume— Get Zomer Gregorio's structured resume, professional experience, technology stack, and current PDF URL.get_tech_stack— Get the technology groups and tools currently published on Zomer's portfolio.list_experience— List Zomer Gregorio's published professional experience in display order.list_projects— List the projects currently published on Zomer Gregorio's portfolio.list_blog_posts— List Zomer Gregorio's published technical articles with bounded pagination, canonical URLs, and optional case-insensitive title search.get_blog_post— Get one published blog post by its canonical public slug.
The documentation MCP at https://zomeru.dev/api/mcp/docs (opens in a new tab) exposes:
get_api_overview— Get API endpoints, protocol scope, quickstart, and versioning guidance.get_authentication_guide— Get the anonymous-access policy and safe credential-handling guidance.get_openapi_spec— Get the canonical OpenAPI 3.2 contract as JSON.
Example client configuration:
{
"mcpServers": {
"zomer-portfolio": {
"url": "https://zomeru.dev/api/mcp"
},
"zomer-portfolio-docs": {
"url": "https://zomeru.dev/api/mcp/docs"
}
}
}The Inspector commands below target the portfolio MCP. Replace https://zomeru.dev/api/mcp with https://zomeru.dev/api/mcp/docs to inspect the documentation MCP.
Current MCP Inspector commands
npx @modelcontextprotocol/inspector --server-url https://zomeru.dev/api/mcp --transport http
npx @modelcontextprotocol/inspector --cli --server-url https://zomeru.dev/api/mcp --transport http --method tools/listAuthentication and safety
Authentication is none for published data and public email/push opt-in. Clients SHOULD NOT send credentials to those routes. The versioned portfolio operations are read-only; notification mutations accept only delivery data and never expose subscription lists. Webhook registration and notification summaries are private admin capabilities.
Caching and freshness
Responses use public HTTP caching and the existing five-minute Sanity revalidation window. Newly published content should appear reasonably quickly without creating a second cache architecture.
Outgoing blog webhooks
The portfolio can send a versioned HTTPS request after a blog is successfully published. Only the
blog.published event is available. Webhook registration is admin-approved: send the endpoint URL,
integration name, and destination type through the contact page (opens in a new tab). The portfolio
owner then registers it through the webhook controls on /admin or the protected
POST /api/notifications/webhooks route. This avoids an unauthenticated arbitrary-URL relay.
Portfolio-owner registration request (the bearer value is the existing blog-generation admin capability, never a developer-supplied credential):
curl -X POST https://zomeru.dev/api/notifications/webhooks -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" --data '{"name":"Example integration","url":"https://example.com/hooks/zomer","destinationType":"generic","events":["blog.published"]}'The same protected API lists non-sensitive summaries with
GET /api/notifications/webhooks and disables one with
DELETE /api/notifications/webhooks/{id}. The admin page can send a direct connectivity test with
POST /api/notifications/webhooks/{id}/test; tests are not persisted as publication events and are
not retried. Generic test requests use the signed webhook.test event type, while Slack and Discord
receive a clearly labeled test message.
Generic destinations receive the following JSON contract:
{
"id": "evt_...",
"type": "blog.published",
"apiVersion": "1",
"createdAt": "2026-08-25T07:00:00.000Z",
"data": {
"blog": {
"id": "sanity-document-id",
"revision": "sanity-revision",
"title": "Post title",
"slug": "post-title",
"excerpt": "Short description",
"publishedAt": "2026-08-25T07:00:00.000Z",
"url": "https://zomeru.dev/blogs/post-title"
}
}
}The registration response returns the generic signing secret once. Store it in a secret manager. The destination URL and signing secret are encrypted at rest; neither is exposed by list or admin summary responses. Each generic request includes:
| Header | Value |
|---|---|
X-Portfolio-Event | blog.published |
X-Portfolio-Delivery | Stable per-destination delivery UUID |
X-Portfolio-Timestamp | Unix timestamp in seconds |
X-Portfolio-Signature | v1= plus HMAC-SHA256 of timestamp.rawBody |
X-Webhook-Version | 1 |
Verify the exact raw request body before parsing JSON and reject timestamps older than five minutes:
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
function constantTimeEqual(actual: string, expected: string) {
const digest = (value: string) => createHash("sha256").update(value).digest();
return timingSafeEqual(digest(actual), digest(expected));
}
function signWebhookPayload(secret: string, timestamp: string, rawBody: string) {
return createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
}
export function verifyWebhookSignature(options: {
rawBody: string;
timestamp: string;
signature: string;
secret: string;
toleranceSeconds?: number;
now?: Date;
}) {
const timestampSeconds = Number(options.timestamp);
if (!Number.isSafeInteger(timestampSeconds)) return false;
const nowSeconds = Math.floor((options.now ?? new Date()).getTime() / 1_000);
if (Math.abs(nowSeconds - timestampSeconds) > (options.toleranceSeconds ?? 300)) return false;
const expected =
"v1=" + signWebhookPayload(options.secret, options.timestamp, options.rawBody);
return constantTimeEqual(options.signature, expected);
}Return any 2xx status after durably accepting a request. Timeouts, 408, 425, 429,
and 5xx responses become eligible for retry after 1 minute, 5 minutes, 30 minutes, and 2 hours,
with at most five attempts total. The portfolio owner starts queued retries from the protected admin
page; the retry endpoint is not public or scheduled. Other 4xx responses fail permanently.
Consumers must use
X-Portfolio-Delivery or the event ID as an idempotency key because a network failure can cause a
successful request to be delivered again.
Choose slack or discord during registration for platform-specific message payloads. Slack
destinations are restricted to official incoming-webhook hosts. Discord destinations are restricted
to official webhook URLs and use wait=true so a successful response confirms acceptance. These
adapters do not receive the generic signature headers because their URL credentials are the platform
authentication mechanism.
For local testing, expose an HTTPS handler with a tunnel such as Cloudflare Tunnel or ngrok, request admin approval for the temporary public URL, preserve the raw body in the handler, and publish only to an explicitly selected development Sanity dataset. Localhost and private/link-local IP targets are rejected by design.
