All docs

Marina API

The surface everything else is built on. One base URL, a bearer token, and an error envelope worth branching on.

This is the whole of Marina over HTTP. The CLI is a client of it and so is MCP, which is why anything either of those can do, this can do.

Call it directly when neither fits: a language with no MCP client, a webhook handler, CI that only has curl.

curl https://marinahost.app/api/v1/vessels   -H "Authorization: Bearer $MARINA_API_TOKEN"

Auth

A bearer token, minted the same way as any other credential:

marina tokens create ci --preset deploy

Every request carries it. There is no session, no cookie and no refresh, and what the token may do is fixed when it is minted. Tokens and scopes covers presets and how to narrow one.

The surface

GET /api/v1/accountWho this token belongs to and what it may do.
GET /api/v1/walletBalance and payment code.
GET /api/v1/resourcesEverything the account runs, in one call.
GET, POST /api/v1/vesselsList apps, create one.
POST /api/v1/vessel-archivesUpload a .zip of a project, multipart. Answers with an id to pass as archiveId when you create the app.
GET, DELETE /api/v1/vessels/{id}One app.
POST /api/v1/vessels/{id}/deployShip it.
GET /api/v1/vessels/{id}/buildsDeploy history.
GET /api/v1/vessels/{id}/logsOutput, streamed.
POST /api/v1/vessels/{id}/rollbackPut an earlier deploy back.
PATCH /api/v1/vessels/{id}/sizeScale it. Upgrades only.
POST /api/v1/vessels/{id}/runOne-off command inside the app. Needs vessel:exec.
GET, PATCH, PUT /api/v1/vessels/{id}/envConfig vars. PATCH merges, PUT replaces, GET needs env:read.
GET, POST /api/v1/sites, DELETE /api/v1/sites/{id}Static sites.
GET, POST /api/v1/databases, DELETE /api/v1/databases/{id}Managed databases.
GET /api/v1/databases/{id}/credentialsConnection details. Needs db:credentials.
POST /api/v1/vessels/{id}/databasesAttach one. DELETE .../{database_id} detaches.
GET /api/v1/domains/searchIs a name available, and what does it cost.
GET, POST /api/v1/domainsOwn and buy.
GET /api/v1/domains/orders/{id}A purchase in flight.
GET, POST /api/v1/domains/customPoint a domain you already own.
POST /api/v1/domains/custom/{id}/verifyRe-check its records and publish the route.
DELETE /api/v1/domains/custom/{id}Stop pointing it here.
GET /api/v1/dns/zones, GET /api/v1/dns/zones/{id}Zones we host, and their records.
POST /api/v1/dns/zones/{id}/recordsAdd a record. PATCH and DELETE on .../records/{rid}.
GET, POST /api/v1/emailEmail plans: list, and buy one for a domain.
POST /api/v1/email/verifyRe-check a plan’s DNS and turn it on.
GET, POST /api/v1/email/mailboxesAddresses on a domain, and create one.
POST /api/v1/email/mailboxes/deleteRemove an address and its mail.
POST /api/v1/email/mailboxes/passwordChange an address’s password.
POST /api/v1/email/tierMove a domain’s plan up or down.
GET /api/v1/operations/{id}A long-running operation’s state.
GET /api/v1/invoicesWhat has been billed.

Read endpoints need the matching :read scope, writes need :write, and anything that spends needs billing:spend as well.

Errors

Every failure has the same shape, so one branch handles all of them:

{
	"error": {
		"code": "INSUFFICIENT_FUNDS",
		"message": "Your wallet is short GHS 187.42."
	}
}

The code is what to branch on; the message is what to show a person. The status is the ordinary HTTP one: 400 for a bad request, 401 unauthenticated, 403 not allowed, 404 missing, 409 conflicting, 402 for money, 429 rate limited.

Codes worth handling by name:

CodeWhat to do
INSUFFICIENT_FUNDSNothing was created or charged. Tell the person how to top up, then retry.
SPEND_LIMIT_EXCEEDEDThis token’s lifetime cap is reached. A new credential is the only way past it.
FORBIDDENThe token lacks the scope. Do not retry; it will never succeed.
CONFLICTUsually the name is taken, or the resource is in a state that forbids this.

When there is no money

A 402 carries the remedy, not just the refusal:

{
	"error": {
		"code": "INSUFFICIENT_FUNDS",
		"message": "Your wallet is short GHS 187.42.",
		"details": {
			"required": "190.00",
			"balance": "2.58",
			"shortfall": "187.42",
			"currency": "GHS",
			"paymentCode": "<your code>",
			"momoNumber": "0558863139",
			"accountName": "ISAAC KWEKU BADU SMITH"
		}
	}
}

shortfall is precomputed so you never do decimal arithmetic on two strings, and every amount is a string at exactly two places. Do not parse money into a float. Nothing was created and nothing was charged, so a retry after the balance arrives is simply the same request again.

Retrying a create safely

A create that times out may still have happened. Send an Idempotency-Key and a repeat returns the original result rather than buying a second one:

curl -X POST https://marinahost.app/api/v1/databases   -H "Authorization: Bearer $MARINA_API_TOKEN"   -H "Idempotency-Key: 9f2c1e7a-order-42"   -H "Content-Type: application/json"   -d '{"name":"store","engine":"postgres","storageGb":1}'

Pick the key from something stable in your own system, so the retry after a crash uses the same one. This is the only protection that closes the window fully: a name check cannot see a create that has paid and is still provisioning.

Buying a domain finishes later

A domain purchase reserves the name, holds the price, and returns an order:

curl https://marinahost.app/api/v1/domains/orders/{id}   -H "Authorization: Bearer $MARINA_API_TOKEN"

Poll that until it settles. Everything else on this API completes while you wait.

Things this API cannot do

Deliberately, and there is no flag for any of them: mint another token, invite a member, change the account, or add storage to an app. Account-level actions need a signed-in session, so a leaked credential cannot mint a replacement for itself or bring in an accomplice.