Bravely Link · For your AI
Bravely Link API
Let your AI build and update your link pages. An agent that can call an API does it with a key. Any chat AI can write the same page for you to paste in.
Paste from a chat
No agent and no key. Give this prompt to ChatGPT or any chat AI. It asks about you and your links, then answers with your whole page as one block of text.
I want you to build my Bravely Link page, a single page that holds all my links. Ask me what you need, one question at a time: my name or brand, a one-line bio, and each link I want with a short label. Suggest a handle (3 to 30 lowercase letters, numbers or dashes). When I say I'm done, reply with only one JSON code block in exactly this format and nothing else:
```json
{
"bravely_link": 1,
"handle": "samrivera",
"title": "Sam Rivera",
"bio": "Trail runner. Photos from the long way round.",
"theme": "classic-dark",
"link_style": "classic",
"links": [
{
"type": "header",
"label": "Start here"
},
{
"label": "My newsletter",
"url": "https://example.com/news",
"subtitle": "Every Sunday"
},
{
"label": "Instagram",
"url": "https://instagram.com/samrivera"
}
]
}
```
Rules: every link needs a "label" and a full "url" starting with https://. A row with "type": "header" has only a label. "subtitle" is optional, at most 100 characters. "theme" is one of: classic-dark, paper-light, sunset, mono, ocean, ember, aurora, forest, blush, sky. "link_style" is one of: classic, pill, tall, outline. The title is at most 120 characters and the bio at most 500. Leave out "avatar_url" unless I give you the address of a picture. Keep labels short. Never invent a link I did not give you.
Then open the builder, choose Import and paste the whole answer. You see the page before anything is made, it opens as a draft, and publishing stays your click.
Quick start
For an agent, or a script, that can make web requests.
Make a key
Sign in at https://link.bravely.dev/ and make a key under API keys. A key is shown once and works only on its owner's pages.
Create your page
Put your key in place of
YOUR_KEYand the handle you want in place ofyour-handle, in the address and in the document.Create a page curl -X PUT https://link.bravely.dev/api/v1/pages/your-handle \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "bravely_link": 1, "handle": "your-handle", "title": "Sam Rivera", "bio": "Trail runner. Photos from the long way round.", "theme": "classic-dark", "link_style": "classic", "links": [ { "type": "header", "label": "Start here" }, { "label": "My newsletter", "url": "https://example.com/news", "subtitle": "Every Sunday" }, { "label": "Instagram", "url": "https://instagram.com/samrivera" } ] }'The answer is the page as it was stored, with a warning for anything it changed. Nothing is public yet.
Publish it
Publish it curl -X POST https://link.bravely.dev/api/v1/pages/your-handle/publish \ -H "Authorization: Bearer YOUR_KEY"Now it is live at
bravely.link/your-handle. To change it later, read the page, edit the document, send it back and publish again. Keep each link’sidand its click counts stay with it.
Or hand your agent one address. It describes every call, field and limit on this page:
https://link.bravely.dev/api/v1The page document
A page is one JSON document. The importer and the API read the same one, so what a chat AI writes and what an agent sends are the same thing.
{
"bravely_link": 1,
"handle": "samrivera",
"title": "Sam Rivera",
"bio": "Trail runner. Photos from the long way round.",
"avatar_url": "https://example.com/sam.jpg",
"theme": "classic-dark",
"link_style": "classic",
"links": [
{
"type": "header",
"label": "Start here"
},
{
"label": "My newsletter",
"url": "https://example.com/news",
"subtitle": "Every Sunday"
},
{
"label": "Instagram",
"url": "https://instagram.com/samrivera"
}
]
}The page
| Field | Holds | What it is |
|---|---|---|
bravely_linkrequired | Always 1 | The format version. Always the number 1. |
handlerequired | Text, 3 to 30 characters | The page's address: https://bravely.link/<handle>. 3 to 30 lowercase letters, numbers or dashes. Capitals, spaces, dots and underscores are rewritten to that form, with a warning. |
title | Text, up to 120 characters | The heading: a name or brand. Left out, it is "". |
bio | Text, up to 500 characters | A short line under the title. Left out, it is "". |
avatar_url | Text, up to 2048 characters, or null | The full https:// address of the profile picture, or null for none. POST /api/v1/media/avatar stores a picture and returns an address to use here. Left out, it is null. |
theme | One of these
| The page's colors and type. Left out, it is "classic-dark". |
link_style | One of these
| The shape of the link buttons. Left out, it is "classic". |
links | A list, up to 100 rows | The page's rows, top to bottom. Left out, it is []. |
Each row of links
| Field | Holds | What it is |
|---|---|---|
id | Text | Set by the service and returned on a read. Send it back unchanged to keep a link's click history; leave it out for a new link. |
type | One of these
| A "link" or "social" row opens a web address. An "email" row opens a new message. A "header" is a section label: it has only a label. Left out, it is "link". |
labelrequired | Text, up to 120 characters | What the row says. |
url | Text, up to 2048 characters | Required unless the row is a header. A full address starting with https:// (http:// also works). For an "email" row, one email address and nothing else. |
subtitle | Text, up to 100 characters | An optional second line under the label. |
image_url | Text | An optional small image on the button. Only an address returned by POST /api/v1/media/link-image is accepted; an image hosted elsewhere is refused. |
enabled | true or false | false keeps the row in the draft and off the published page. Left out, it is true. |
What a read adds (a write ignores these)
| Field | Holds | What it is |
|---|---|---|
status | One of these
| Where the page stands: a draft is not public, a published page is live, and a suspended page was taken down and can't be changed. |
url | Text, or null | The published page's address; null until it is published. |
published_at | A date and time, or null | When the page was last published; null until it is. |
updated_at | A date and time | When the draft last changed. |
warnings | A list of sentences | What a write changed or left out, one sentence each. |
The calls
Every call is at https://link.bravely.dev and answers JSON. Where a call needs a key, send it as a header: Authorization: Bearer YOUR_KEY.
| Call | Key | What it does |
|---|---|---|
GET /api/v1 | Open | The manifest: every call, field and limit, for an agent to read first. |
GET /api/v1/schema | Open | JSON Schema of the page document. |
GET /api/v1/openapi.json | Open | OpenAPI 3.1 description of these calls. |
GET /api/v1/prompt.txt | Open | A prompt a person can give a chat AI to have their page written, for pasting into the importer. |
GET /api/v1/pages | Needs a key | Your pages, each as a page document. |
GET /api/v1/pages/{handle} | Needs a key | One page's draft, as a page document. |
PUT /api/v1/pages/{handle} | Needs a key | Create the page, or replace its draft with the document you send. Sending the same document twice changes nothing the second time. |
POST /api/v1/pages/{handle}/publish | Needs a key | Publish the draft. Until you call this, the public page shows what was last published. |
DELETE /api/v1/pages/{handle} | Needs a key | Delete the page and free its handle. |
GET /api/v1/pages/{handle}/stats | Needs a key | Click counts for the last 7, 30 or 90 days (?days=). |
POST /api/v1/media/avatar | Needs a key | Store a profile picture. Send the image's bytes as the body; the answer's url goes in avatar_url. |
POST /api/v1/media/link-image | Needs a key | Store a link's image. Send the image's bytes as the body; the answer's url goes in a link's image_url. |
Limits and errors
Limits
| Keys on one account | 5 |
|---|---|
| Requests with one key | 60 a minute |
| Pages on one account | 10 |
| Publishes on one account | 20 an hour |
| An uploaded image | JPEG, PNG or WebP, up to 2 MB |
| Profile pictures uploaded | 30 an hour |
| Link images uploaded | 60 an hour |
A field’s own limits are in the page document. Images are uploaded, never fetched: a link's image_url must be an address a media upload returned.
When a call is refused
The answer is JSON with one field, { "error": "one or more plain sentences" }. Every error names the field and the fix. When a document has several problems, the one string lists them all. A 429 carries a Retry-After header in seconds.
| Status | When |
|---|---|
400 | The document has problems; the error lists each field and its fix.A link's address can't be published. |
401 | No API key, or one that is not valid. |
403 | The page is suspended. |
404 | No page with that handle on this account. |
409 | The handle is taken, or the account already has 10 pages.The page has no handle of its own yet. |
413 | The body is too large.The image is too large. |
415 | The body is not a JPEG, PNG or WebP image. |
429 | More than 60 requests in a minute with this key.More than 20 publishes in an hour on this account, or too many requests with this key. |