Vesting API
Manage token vesting contracts.
Get Vesting Status
GET /vesting/:contractId
Retrieve detailed vesting contract information.
Response
{
"contractId": "VEST123...",
"launchId": "launch_abc123",
"beneficiary": "8vP2...",
"mint": "ABC123...",
"total": 1000000000,
"claimed": 250000000,
"unlocked": 500000000,
"claimable": 250000000,
"locked": 500000000,
"schedule": {
"start": 1700000000,
"cliff": 1710000000,
"end": 1735000000,
"cliffMonths": 6,
"durationMonths": 24
},
"status": "active",
"createdAt": "2026-02-15T12:00:00Z"
}schedule.cliff (and the underlying Streamflow stream.cliff) is an absolute unix timestamp — the moment the cliff ends — not an offset from start. unlocked / claimable are computed against that absolute value: they are 0 before the cliff and grow linearly from the cliff to end.
Status Values
| Status | Description |
|---|---|
active | Vesting in progress |
cliff | In cliff period |
completed | All tokens vested |
cancelled | Contract cancelled |
Claim Tokens
POST /vesting/:contractId/claim
Claim unlocked but unclaimed tokens.
Ownership. The claim is signed by Etch; the unlocked tokens are always paid to the stream recipient (the launch
beneficiary), never to the caller. The authenticated wallet must be that recipient (or the wallet that created the launch), otherwise the API returns403 { "error": "not_owner" }.
Request Body
{
"amount": "all" // Or specific amount in base units
}Response
{
"signature": "5abc...",
"claimed": 250000000,
"newClaimable": 0,
"newTotalClaimed": 500000000
}Build a Claim Transaction (non-custodial)
POST /vesting/:contractId/tx
Preferred over the custodial claim above. Etch builds an unsigned Streamflow withdraw with the stream recipient as authority and fee payer; the recipient’s wallet signs it and the client sends it. See the Transactions API for the response object and signing flow.
The authenticated wallet must be the stream’s recipient (the address the
stream pays out to) — otherwise the API returns 403 { "error": "not_recipient" }.
If the recipient’s token account does not exist yet, its creation is included
in the same transaction (paid by the recipient).
Request Body
{ "action": "claim", "amount": 12.5 }amount is in whole tokens (9 decimals). Omit it to claim everything currently
claimable.
Response
{
"transaction": "<base64 legacy Transaction, unsigned>",
"version": "legacy",
"feePayer": "<recipient wallet>",
"blockhash": "…",
"lastValidBlockHeight": 123,
"signers": ["<recipient wallet>"],
"summary": "Claim all claimable vested tokens from VEST…1234"
}Errors
| Status | error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing / invalid JWT |
| 400 | invalid_request | Body failed validation |
| 403 | not_recipient | Wallet is not the stream recipient |
| 404 | vesting_not_found | Stream could not be read |
| 501 | unsupported | The Streamflow SDK cannot build an unsigned withdraw — fall back to the custodial POST /vesting/:contractId |
| 500 | build_failed | RPC / SDK error while building (message) |
List Vesting Contracts
GET /launches/:launchId/vesting
Get all vesting contracts for a launch.
Response
{
"contracts": [
{
"contractId": "VEST123...",
"beneficiary": "8vP2...",
"total": 1000000000,
"status": "active"
}
]
}Create Manual Vesting (Admin)
POST /admin/vesting
Create a vesting contract manually (requires admin auth).
Request Body
{
"mint": "ABC123...",
"beneficiary": "8vP2...",
"amount": 1000000000,
"cliffMonths": 6,
"durationMonths": 24
}Response
{
"contractId": "VEST456...",
"transaction": "5def...",
"status": "created"
}Cancel Vesting (Admin)
POST /vesting/:contractId/cancel
Cancel a vesting contract and return remaining tokens (requires admin auth).
Response
{
"signature": "5ghi...",
"returned": 500000000,
"status": "cancelled"
}Webhook Events
Vesting emits these webhook events:
| Event | Description |
|---|---|
vesting.created | Contract created |
vesting.cliff_reached | Cliff period ended |
vesting.claimed | Tokens claimed |
vesting.completed | All tokens vested |
vesting.cancelled | Contract cancelled |
Examples
Check and Claim
async function checkAndClaim(authToken: string, contractId: string) {
// Check status
const status = await fetch(
`https://etch.film.fun/api/vesting/${contractId}`,
{ headers: { Authorization: `Bearer ${authToken}` } }
).then(r => r.json());
console.log(`Claimable: ${status.claimable}`);
if (status.claimable > 0) {
// Claim tokens
const claim = await fetch(
`https://etch.film.fun/api/vesting/${contractId}/claim`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: "all" })
}
).then(r => r.json());
console.log(`Claimed! Tx: ${claim.signature}`);
return claim;
}
return null;
}Error Codes
| Code | Description |
|---|---|
vesting_not_found | Contract doesn’t exist |
nothing_to_claim | No tokens available |
vesting_complete | Already fully claimed |
unauthorized_claim | Wrong beneficiary |
contract_cancelled | Contract was cancelled |