Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Cavuno v1 REST Transactions API
version: 1.0.0
description: 'The Cavuno API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs. ## Base URL
Production: `https://api.cavuno.com/v1`
All API requests must be made over HTTPS. Calls made over plain HTTP will fail. ## Authentication
The Cavuno API uses API keys to authenticate requests. You can view and manage your API keys in the Cavuno dashboard, under Settings → Developer → API keys. Cavuno API keys have the prefix `cavuno_live_`. Your API keys carry many privileges, so be sure to keep them secure. Do not share your secret API keys in publicly accessible areas such as GitHub, client-side code, and so forth. Authentication to the API is performed via Bearer auth. Provide your API key as the bearer token in the `Authorization` header:
```bash
curl https://api.cavuno.com/v1/jobs \
-H "Authorization: Bearer YOUR_SECRET_API_KEY"
```
API requests without authentication will fail. ## Rate limits
The Cavuno API has rate limits in place to ensure stability and reliability. By default, API keys are limited to 100 requests per minute. If you exceed your limit, requests return an HTTP 429 response with a `Retry-After` header indicating when you can try again. If you have a use case that needs a higher limit, contact support. ## Idempotency
The API supports idempotency for safely retrying requests without accidentally performing the same operation twice. For example, if a request to create a job fails due to a network connection error, you can safely retry the request with the same idempotency key, and we''ll guarantee that only one job is created. To perform an idempotent request, provide an additional `Idempotency-Key` header on any `POST`, `PATCH`, or `DELETE` request:
```bash
curl https://api.cavuno.com/v1/jobs \
-X POST \
-H "Authorization: Bearer cavuno_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d ''{ "title": "Senior Engineer" }''
```
## Errors
Cavuno uses conventional HTTP response codes to indicate the success or failure of an API request. In general: codes in the `2xx` range indicate success, codes in the `4xx` range indicate an error that failed given the information provided (e.g., a required parameter was omitted), and codes in the `5xx` range indicate an error with Cavuno''s servers (these are rare). Error responses include a JSON body with details:
```json
{
"error": {
"code": "validation_bad_request",
"message": "Field ''title'' is required.",
"requestId": "req_..."
}
}
```
Some errors carry an additional `details` field with structured context, such as per-field validation issues. ### Plan upgrade hints
When an Operator API request is refused because of the board''s Cavuno plan, the error also carries an `upgrade` object. It names the capability that blocked the request, the cheapest self-serve plan that unlocks it (when that can be told), and the hosted billing endpoints that start the upgrade:
```json
{
"error": {
"code": "settings_plan_required_for_branding_removal",
"message": "Removing Cavuno branding requires a plan that includes branding removal.",
"requestId": "req_...",
"upgrade": {
"capability": "canRemoveBranding",
"requiredPlan": "basic",
"checkout": { "method": "POST", "path": "/v1/billing/checkout" },
"changePlan": { "method": "POST", "path": "/v1/billing/upgrade" }
}
}
}
```
Call `GET /v1/billing/subscription` first. With no active subscription, use `checkout`; with one, use `changePlan`. Send `{ "planKey": "" }` and give the returned `url` to a person: both endpoints return a hosted page where a person confirms the purchase. Nothing is charged until they do. Each `path` is relative to the API origin (`https://api.cavuno.com`), like `Location` headers and list `url` fields, so do not join it to a base URL that already ends in `/v1`.'
contact:
name: Cavuno
url: https://cavuno.com
servers:
- url: https://api.cavuno.com/v1
description: Production
security:
- bearerAuth: []
tags:
- name: Transactions
description: 'Unified billing ledger: job-posting orders and employer-subscription transactions.'
paths:
/transactions:
get:
summary: List transactions
description: 'List revenue ledger rows for the tenant''s board. Cursor-paginated. Filters: status, kind, and search (full-text, relevance-ordered). Past-due is not a stored status. Derive it client-side from invoiceDueAt.'
parameters:
- schema:
type: string
minLength: 1
required: false
name: cursor
in: query
- schema:
type: integer
minimum: 1
maximum: 100
required: false
name: limit
in: query
- schema:
type: string
enum:
- paid
- open
- void
- uncollectible
required: false
name: status
in: query
- schema:
type: string
enum:
- card_post
- invoice
- subscription
- service
required: false
name: kind
in: query
- schema:
type: string
minLength: 1
required: false
name: search
in: query
responses:
'200':
description: Paginated transactions with sortedBy.
content:
application/json:
schema:
type: object
properties:
object:
type: string
enum:
- list
url:
type: string
example: /v1/transactions
hasMore:
type: boolean
nextCursor:
type:
- string
- 'null'
data:
type: array
items:
$ref: '#/components/schemas/Transaction'
sortedBy:
type: string
enum:
- date
- relevance
description: 'Sort mode: "relevance" when `search` is present, otherwise "date" (date-desc).'
required:
- object
- url
- hasMore
- nextCursor
- data
- sortedBy
default:
description: An error. Every non-2xx response uses the same envelope; see the Errors section of the introduction.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: listTransactions
/transactions/{id}:
get:
summary: Get a transaction
description: Fetch one transaction by id. Cross-tenant ids return 404 (indistinguishable from missing).
parameters:
- schema:
type: string
minLength: 1
description: The transaction ID.
required: true
description: The transaction ID.
name: id
in: path
responses:
'200':
description: The transaction.
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
default:
description: An error. Every non-2xx response uses the same envelope; see the Errors section of the introduction.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: getTransaction
/transactions/{id}/refunds:
post:
summary: Refund a transaction
description: 'Refunds a paid sale in full or in part on the board''s connected Stripe account, and records whether the buyer keeps what it granted. Send an `Idempotency-Key`: it becomes the Stripe idempotency key, so a retry creates the refund once. Without one, each call is a new refund (never more than is left). Returns the updated transaction. Needs `plans.manage`, the permission the dashboard checks, and `billing.read`, since the response is the transaction with the customer email.'
parameters:
- schema:
type: string
minLength: 1
description: The transaction ID.
required: true
description: The transaction ID.
name: id
in: path
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefundTransactionBody'
responses:
'200':
description: The refunded transaction.
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'400':
description: '`amountCents` missing or not a positive integer (`validation_bad_request`).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Missing `plans.manage` or `billing.read`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Missing, or another board's transaction (`transactions_not_found`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: '`transaction_not_refundable`: not paid, or paid outside Stripe. `refund_amount_exceeds_refundable`: more than is left (`error.details.refundableCents`). `transaction_disputed`: the payment has an open or lost dispute. `access_not_removable`: `removeAccess` sent for a sale with no one-time access.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: '`stripe_not_connected`: the board has no connected Stripe account. `refund_refused`: Stripe refused to create the refund; retrying will not work. `refund_failed`: Stripe created the refund but it failed or was canceled, so no money moved and access is unchanged.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'502':
description: '`stripe_error`: any other Stripe error (network, rate limit, outage). Retry with the same `Idempotency-Key`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: refundTransaction
/transactions/{id}/remove-access:
post:
summary: Remove a transaction's access
description: Revokes what a one-time sale granted (job post credits, a membership, an access pass, a digital product download) without refunding. Removing access that is already removed changes nothing. Returns the updated transaction. Needs `plans.manage`, the permission the dashboard checks, and `billing.read`, since the response is the transaction with the customer email.
parameters:
- schema:
type: string
minLength: 1
description: The transaction ID.
required: true
description: The transaction ID.
name: id
in: path
responses:
'200':
description: The transaction with its access removed.
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'403':
description: Missing `plans.manage` or `billing.read`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Missing, or another board's transaction (`transactions_not_found`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: '`access_not_removable`: a subscription (access follows it) or a sale not linked to a purchase. `transaction_not_refundable`: the sale is not paid.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: '`stripe_not_connected`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'502':
description: '`stripe_error`. Retry.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: removeTransactionAccess
/transactions/{id}/restore-access:
post:
summary: Restore a transaction's access
description: Gives back what a one-time sale granted after it was removed, whether a refund, a dispute or the operator removed it. Restoring access that is live changes nothing. Refused while the payment has an open or lost dispute. Returns the updated transaction. Needs `plans.manage`, the permission the dashboard checks, and `billing.read`, since the response is the transaction with the customer email.
parameters:
- schema:
type: string
minLength: 1
description: The transaction ID.
required: true
description: The transaction ID.
name: id
in: path
responses:
'200':
description: The transaction with its access restored.
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'403':
description: Missing `plans.manage` or `billing.read`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Missing, or another board's transaction (`transactions_not_found`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: '`access_not_removable`: a subscription (access follows it) or a sale not linked to a purchase. `transaction_not_refundable`: the sale is not paid. `transaction_disputed`: the payment has an open or lost dispute. `access_restore_blocked`: the company holds another membership now.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: '`stripe_not_connected`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'502':
description: '`stripe_error`. Retry.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: restoreTransactionAccess
/transactions/{id}/resend-invoice:
post:
summary: Resend an invoice email
description: 'Emails the invoice for an open job posting invoice to its customer again, with the same branded email as the first one. The email is sent in the background: a `202` means it was queued. Requires `plans.manage`.'
parameters:
- schema:
type: string
minLength: 1
description: The transaction ID.
required: true
description: The transaction ID.
name: id
in: path
responses:
'202':
description: The invoice email was queued. Returns the transaction, unchanged.
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'404':
description: Transaction not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: The transaction is not a job posting invoice (`transactions_invoice_action_unsupported`), or the invoice is already paid, void or uncollectible (`transactions_invoice_not_open`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: resendTransactionInvoice
/transactions/{id}/void-invoice:
post:
summary: Void an invoice
description: Voids an open job posting invoice so it can no longer be paid. When the job was set to publish on payment, its unpublished draft is archived; when it was published on issue, the job is moved back to draft. This cannot be undone. Requires `plans.manage`.
parameters:
- schema:
type: string
minLength: 1
description: The transaction ID.
required: true
description: The transaction ID.
name: id
in: path
responses:
'200':
description: 'The invoice was voided. Returns the transaction with `status: void`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'404':
description: Transaction not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: The transaction is not a job posting invoice (`transactions_invoice_action_unsupported`), or the invoice is already paid, void or uncollectible (`transactions_invoice_not_open`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'502':
description: The payment provider could not void the invoice right now (`transactions_invoice_provider_error`). Nothing was changed; retry later.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-audience: operator
tags:
- Transactions
operationId: voidTransactionInvoice
components:
schemas:
Transaction:
type: object
properties:
id:
type: string
object:
type: string
enum:
- transaction
kind:
type: string
enum:
- card_post
- invoice
- subscription
- service
status:
type: string
enum:
- paid
- open
- void
- uncollectible
method:
type: string
enum:
- card
- invoice
amountCents:
type: integer
description: Amount in currency minor units (cents for most currencies).
currency:
type: string
description:
type: string
customerLabel:
type: string
customerEmail:
type:
- string
- 'null'
date:
type: string
description: ISO-8601 transaction date.
invoiceDueAt:
type:
- string
- 'null'
description: ISO-8601 invoice due date, or null. Past-due is client-derived from this field.
hostedInvoiceUrl:
type:
- string
- 'null'
receiptUrl:
type:
- string
- 'null'
methodBrand:
type:
- string
- 'null'
methodLast4:
type:
- string
- 'null'
methodType:
type:
- string
- 'null'
publishTiming:
type:
- string
- 'null'
enum:
- on_issue
- on_payment
- null
refundedAmountCents:
type: integer
description: Cumulative amount refunded, in currency minor units; 0 when none. A refund that failed or was canceled does not count.
refundStatus:
type: string
enum:
- none
- partial
- full
description: From `refundedAmountCents` against `amountCents`.
disputeStatus:
type:
- string
- 'null'
enum:
- open
- won
- lost
- null
description: The payment's dispute on the board's Stripe account, or null.
accessRemoved:
type: boolean
description: 'What the sale granted is revoked: by a refund, a dispute, or the operator.'
accessRemovable:
type: boolean
description: The sale is a one-time purchase, so remove-access and restore-access apply. False for subscriptions (access follows the subscription) and for sales not linked to a purchase.
required:
- id
- object
- kind
- status
- method
- amountCents
- currency
- description
- customerLabel
- customerEmail
- date
- invoiceDueAt
- hostedInvoiceUrl
- receiptUrl
- methodBrand
- methodLast4
- methodType
- publishTiming
- refundedAmountCents
- refundStatus
- disputeStatus
- accessRemoved
- accessRemovable
RefundTransactionBody:
type: object
properties:
amountCents:
type: integer
description: 'The amount to refund, in the transaction currency''s minor units. Required: send the full remaining amount (`amountCents` minus `refundedAmountCents`) for a full refund.'
exclusiveMinimum: 0
removeAccess:
type: boolean
description: 'Whether the buyer loses what the sale granted. Only for a one-time purchase (`accessRemovable: true`). Omitted: a refund reaching the full amount removes access and a partial one keeps it.'
required:
- amountCents
additionalProperties: false
PlanUpgradeHint:
type: object
properties:
capability:
type: string
enum:
- activeJobsLimit
- teamSeatLimit
- canBePublic
- canRemoveBranding
- apiAccess
description: The plan capability that blocked the request, named as in `GET /v1/billing/subscription` `capabilities`.
requiredPlan:
type: string
enum:
- starter
- basic
- grow
- advanced
- scale
description: The cheapest self-serve plan that unlocks the action. Pass it as `planKey`. Omitted when it cannot be told without more information; read `GET /v1/billing/subscription` and pick a higher plan.
checkout:
type: object
properties:
method:
type: string
enum:
- POST
path:
type: string
enum:
- /v1/billing/checkout
required:
- method
- path
description: Starts a hosted checkout when the board has no active subscription (for example during a trial).
changePlan:
type: object
properties:
method:
type: string
enum:
- POST
path:
type: string
enum:
- /v1/billing/upgrade
required:
- method
- path
description: Starts a hosted plan change when the board already has an active subscription.
required:
- capability
- checkout
- changePlan
description: 'Present only on Operator API errors caused by the board''s Cavuno plan. Read `GET /v1/billing/subscription` to choose between `checkout` and `changePlan`, call it with `{ "planKey": requiredPlan }`, and give the returned `url` to a person to confirm. Each `path` is relative to the API origin, not to a `/v1` base URL. Both endpoints need `billing.manage`. Board API errors never include it.'
Error:
type: object
properties:
error:
type: object
properties:
code:
type: string
description: Machine-readable error code, such as `validation_bad_request` or `jobs_not_found`.
message:
type: string
description: Human-readable explanation.
requestId:
type: string
description: Unique id for this request. Include it when contacting support.
details:
description: Structured context for some errors, such as per-field validation issues.
upgrade:
$ref: '#/components/schemas/PlanUpgradeHint'
required:
- code
- message
- requestId
required:
- error
description: The error envelope returned by every non-2xx response.
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: cavuno_live_<keyId>_<secret>
description: Use your secret API key to authenticate. Pass it as a Bearer token in the `Authorization` header. You can view and manage your API keys in the Cavuno dashboard.
externalDocs:
description: Cavuno API reference
url: https://cavuno.com/docs/api