CurlShip Edits API
Self-service listing changes, with proof of ownership.
Self-service listing changes, with proof of ownership.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/curlship-com:curlship-com-edits-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Curlship Com Edits API
version: 1.0.0
contact:
name: CurlShip Support
email: support@curlship.com
url: https://curlship.com
license:
name: Proprietary
description: 'Operations tagged Edits across 2 of this provider''s published API definitions: curlship-com-openapi.json, curlship-com-openapi.yml. Each path carries the servers of the definition it was published in.'
servers:
- url: https://curlship.com
description: Production
tags:
- name: Edits
description: Self-service listing changes, with proof of ownership.
paths:
/api/edit-request:
post:
tags:
- Edits
summary: Ask to change your listing
description: 'Starts a change; applies nothing. Prove ownership one of two ways.
`proof: "email"` mails a one-time confirmation link to the address the listing is registered to — the response never contains the token.
`proof: "domain"` returns a token you must publish at `/.well-known/curlship-verify.txt` (as a whole line) or as `` on **both** the currently listed site and the new one, then confirm via `/api/edit-confirm`. Publishing it only on the new domain proves you own the destination, not the listing, and is refused — as is a domain proof when the listed site is unreachable.
Changing `email` always requires `proof: "email"`. Requests expire after 24 hours. Rate limits: 5/hour per IP, 3/day per listing.
**Refreshing stale metadata.** Send only `listing_id` and `proof` — no `url`, no `email` — to mean "re-read the title, description and image from my page". Use it when your site''s copy changed and the directory card still shows what it said on the day you submitted. The URL is not modified, the badge is not reset, and the page scraped is always the one already on the listing. A refresh whose fetch fails, or that comes back with a bot-wall, parking or error-page title, is refused rather than written.
Confirming a refresh takes **two** calls to `/api/edit-confirm`: the first returns `applied: false` with the proposed `changed` values and an `apply` digest and writes nothing; send that digest back to save it. The page is re-read on apply, so if it changed in between you get a fresh preview rather than a write you never saw.
The body cannot set a title, description, or image. Those are copied from the page (`og:title`, `og:description`, `og:image`, else the title tag and the description meta). Emailing replacement copy does not change the card.
**Tracking params on your outbound link.** A paid listing (Bronze and up) can send `tracking`, a JSON object that replaces the default `?ref=curlship` on every link to the site. Keys are `ref` or `utm_*` (lowercase letters, digits, underscores), at most 10; values are 1-100 of `A-Z a-z 0-9 . _ ~ -`. Send `{}` to clear back to `?ref=curlship`. Stored params survive a lapse and return on re-upgrade; a free listing is refused (403).
**Removing a free listing.** Send `remove: true` with `listing_id` and `proof`, and no `url` or `email`. Confirm it the same way as any other edit. The registered address gets a 7-day undo link. A paid listing, or the listing that holds the Featured spot, is refused (409) and stays a mail to support@curlship.com, because a payment is attached. One listing per root domain: to point the same listing at a different page, send a new `url` instead of `remove`.'
operationId: createEditRequest
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- listing_id
properties:
listing_id:
type: integer
example: 2142
url:
type: string
format: uri
description: New product URL
email:
type: string
format: email
description: New registered address (needs proof=email)
remove:
type: boolean
description: true takes a free listing offline. Send no url and no email with it. A paid listing or the Featured holder is refused.
tracking:
type: object
additionalProperties:
type: string
example:
utm_source: yours
description: 'Custom tracking params for the outbound link — paid tiers only (Bronze and up, 403 for free). Keys: ref or utm_*; at most 10. Values: 1-100 of A-Z a-z 0-9 . _ ~ -. An empty object clears back to ?ref=curlship.'
proof:
type: string
enum:
- email
- domain
default: email
example:
listing_id: 2142
url: https://new-url.example/
proof: domain
responses:
'201':
description: Request created — pending proof
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
example: true
request_id:
type: integer
proof:
type: string
enum:
- email
- domain
status:
type: string
example: pending
token:
type: string
description: Domain proof only — publish this. Never returned for email proof.
expires_at:
type: string
publish:
type: array
description: Domain proof only — where the token has to appear.
items:
type: object
message:
type: string
'400':
description: Bad body, bad URL, bad tracking params, or email change without email proof
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Custom tracking params on a free listing — a paid perk from Bronze up
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: No active listing with that id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: Removal refused because the listing is paid or holds the Featured spot, or the registered address has permanently rejected mail
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded. Retry-After is the wait in seconds. RateLimit-Policy names the window that was hit. RateLimit repeats that name with r=0 and t equal to Retry-After. Windows: submit 10 per 3600s, verify 30 per 3600s, checkout 30 per 3600s, edit 5 per 3600s per IP, edit-listing 3 per 86400s per listing.'
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
minimum: 1
RateLimit-Policy:
description: 'The window that was hit. Example: "submit";q=10;w=3600.'
schema:
type: string
RateLimit:
description: 'Remaining quota. Example: "submit";r=0;t=120.'
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'503':
description: Confirmation email could not be sent
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
servers:
- url: https://curlship.com
description: Production
/api/edit-request/{id}:
get:
tags:
- Edits
summary: Poll an edit request
description: 'Status of a request: pending, applied, expired or cancelled. Never echoes the token.'
operationId: getEditRequest
parameters:
- name: id
in: path
required: true
description: Edit request id
schema:
type: integer
example: 7
responses:
'200':
description: The request
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
example: true
request:
type: object
properties:
id:
type: integer
listing_id:
type: integer
proof:
type: string
status:
type: string
enum:
- pending
- applied
- expired
- cancelled
changes:
type: object
created_at:
type: string
expires_at:
type: string
'400':
description: Id is not an integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: No request with that id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
servers:
- url: https://curlship.com
description: Production
/api/edit-confirm:
post:
tags:
- Edits
summary: Confirm an edit request and apply it
description: 'For a domain proof, this re-fetches every affected site and checks the token before writing anything. For an email proof the same thing happens when the recipient opens the mailed link and presses the confirm button (the link itself only displays the pending change — mail scanners fetch links, and the token is single use), so calling this endpoint by hand is only needed if you kept the token.
On success: the change is written, every field is logged with its old value, the registered address is emailed an undo link valid 7 days, and a URL change resets the badge to unverified.
**A metadata refresh takes two calls here.** The first returns `applied: false`, `status: "preview"`, the proposed `changed` values and an `apply` digest, and writes NOTHING — do not read a 200 as "done", check `applied`. Send the digest back as `apply` to save it. The page is re-read on apply and the digest re-checked, so if it changed in between you get a fresh preview instead of a write nobody saw. URL and email changes stay one-step: those values came from you and are not re-read.'
operationId: confirmEditRequest
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- request_id
- token
properties:
request_id:
type: integer
example: 7
token:
type: string
apply:
type: string
description: Metadata refresh only. The digest returned by the preview call. Omit it to get the preview; send it back to write. Ignored for url/email changes.
example:
request_id: 7
token: Kx9…
responses:
'200':
description: Applied, OR — for a metadata refresh sent without a matching `apply` digest — a preview that wrote nothing. Check `applied`.
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
example: true
applied:
type: boolean
description: false means nothing was written — this is a preview.
status:
type: string
enum:
- preview
description: Present only on a preview.
changed:
type: object
nullable: true
description: 'Preview only: field -> {from, to}. null when nothing would change.'
apply:
type: string
description: 'Preview only: send this back to write.'
listing_id:
type: integer
description: Preview only.
url:
type: string
description: 'Preview only: the page that was read.'
then:
type: object
description: 'Preview only: the exact follow-up call that writes.'
message:
type: string
listing:
type: object
properties:
id:
type: integer
example: 2076
url:
type: string
format: uri
example: https://yourapp.com/
link:
type: string
format: uri
example: https://yourapp.com/?ref=curlship
description: The URL a visitor is actually sent to — url plus tracking params. Free listings carry ?ref=curlship; paid ones carry the owner's utm_*/ref params (set via POST /api/edit-request tracking).
tier:
type: string
enum:
- free
- bronze
- silver
- gold
- platinum
title:
type:
- string
- 'null'
example: YourApp — ship faster
description:
type:
- string
- 'null'
image:
type:
- string
- 'null'
format: uri
has_badge:
type: boolean
description: True if a dofollow link back to curlship.com was found on the site.
created_at:
type:
- string
- 'null'
format: date-time
example: '2026-10-12T09:04:05Z'
description: When the listing was created, UTC.
category:
oneOf:
- type: object
properties:
slug:
type: string
example: developer-tools
description: The leaf slug when classified that deep, else the group.
name:
type: string
example: Developer tools
group:
type: string
example: developer-tools
description: The top-level group this listing belongs to.
- type: 'null'
description: The listing's category, or null while it is unclassified. Set by the off-server classifier — submitters cannot choose one.
'400':
description: Missing request_id or token
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: No request with that id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: Bad token, expired, already used, or proof not satisfied
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'Rate limit exceeded. Retry-After is the wait in seconds. RateLimit-Policy names the window that was hit. RateLimit repeats that name with r=0 and t equal to Retry-After. Windows: submit 10 per 3600s, verify 30 per 3600s, checkout 30 per 3600s, edit 5 per 3600s per IP, edit-listing 3 per 86400s per listing.'
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
minimum: 1
RateLimit-Policy:
description: 'The window that was hit. Example: "submit";q=10;w=3600.'
schema:
type: string
RateLimit:
description: 'Remaining quota. Example: "submit";r=0;t=120.'
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
servers:
- url: https://curlship.com
description: Production
components:
schemas:
Error:
type: object
properties:
ok:
type: boolean
example: false
error:
type: string
example: url is required and must start with http
required:
- ok
- error
externalDocs:
description: Badge guide & instant verify
url: https://curlship.com/badge
x-refined-from:
- curlship-com-openapi.json
- curlship-com-openapi.yml