Skip to main content
cc_heistcontracts doesn’t ship any heists — it just runs the marketplace, slot economy, and dashboard around them. The whole integration lives in one RegisterContract call. No separate exports need to live on the heist resource.
This page is for developers implementing their own heists. All of our heists already handles the registering of contracts.

The minimum integration

For a custom heist you control:
That’s the whole integration. Three exports total: RegisterContract (once), FinishHeist (whenever the heist ends, fired from inside the handler above), and optionally ApplyCooldown if the contract has a cooldown set.
Why register the completion handler inside entry_point rather than at the top of the file?
  • It only listens while a heist is actually running.
  • The handler closes over runtimeData.group, so it knows which run the event belongs to. A top-level handler would have to figure that out from the event payload alone.
Removing the handler on first fire (RemoveEventHandler(handler)) prevents the listener list from growing every time the heist starts. The wider pattern is “one handler per run, scoped to that run.”

The three exports

And one optional helper:

RegisterContract

Signature

data is the contract table. entry_point(runtimeData) is called when a player starts the contract from a slot. opts is an optional table that currently accepts two callbacks:
Both are optional. The Give Up button on the dashboard is hidden when cancel isn’t set — the integration shouldn’t offer a control that does nothing. RegisterContract returns the contract id.

Field reference

What happens after registration

1

Contract enters the registry

Stored in memory, keyed by id. Survives until the resource restarts.
2

Marketplace drips a listing

On the next dripIntervalMs tick, the marketplace may pick your contract (weighted by weight) and add a new listing. The same contract can be listed multiple times in parallel.
3

Player buys a listing

The server validates VICE balance, level, and slot availability, debits the price, fills a slot, and removes the listing.
4

Player starts a slot

The server checks the contract isn’t on cooldown and the player isn’t already in another heist, marks the slot active, rolls a random location if you didn’t pass one, and calls your entry_point function.
5

Heist runs

Your code runs. The dashboard, if reopened, shows an “in progress” panel with elapsed time and (if you provided opts.cancel) a Give Up button.
6

Heist ends

Your code calls FinishHeist(group, success) for the whole group, or FinishContract(src, success) for one player. cc_heistcontracts consumes each player’s slot, awards rewards on success, records stats, and pushes a fresh profile to their dashboard.

FinishHeist and FinishContract

Two ways to signal completion:
  • FinishHeist(groupName, success) — call once with a cc_lib.Groups name. Walks every member and finishes their slot.
  • FinishContract(src, success) — single-player variant. Use when you already have a specific source id and don’t want to walk a group.
Both pass true for success or false for failure. What happens on success:
  • The player’s active slot is consumed (gone after this call).
  • reward.vice is credited to their VICE balance with a ledger entry.
  • reward.xp is credited to their profile, advancing level if a threshold is crossed.
  • stats.completed and stats.byContract[id].completed increment.
  • stats.favoriteContract updates if this contract is now their most-completed.
  • A fresh profile snapshot is pushed to the player’s dashboard.
What happens on failure:
  • The slot is consumed.
  • stats.failed and stats.byContract[id].failed increment.
  • No rewards.
Idempotent. A second call for the same player while no heist is active returns (false, 'not_active'). Safe to call defensively from multiple code paths.

Picking your finish moment

Where you call FinishHeist defines what “the heist is over” means for your players. Typical patterns:
  • Objective-driven: when the last objective is completed (cc_lib.TaskUI.Create accepts an onComplete callback that fires when every task hits 'complete').
  • Escape-driven: when the player crosses an escape boundary or hands the goods off.
  • Timer-driven: when the heist’s overall timer runs out — call with success = false.
FinishHeist is a pure server-side state mutation — players don’t see anything happen visually from it alone. Your heist resource is still responsible for the in-world feedback (notifications, animations, world reset).

opts.cancel — handling Give Up

When a player presses Give Up on the dashboard, cc_heistcontracts:
  1. Calls your opts.cancel(ctx) callback. Your job here is to tear the heist down for ctx.group (notify players, hide UI, reset world state).
  2. After the callback returns, automatically finishes every member of ctx.group as failure. You don’t need to call FinishContract yourself for cancellation.
If you don’t provide opts.cancel, the Give Up button is hidden on the dashboard’s active-heist panel. The player can still finish the heist normally, or wait it out — they just can’t bail mid-run. This is the right default for heists that don’t have a clean abort path.

Why this isn’t an export on your resource

opts.cancel is a closure registered alongside your contract data. It lets a third-party heist integration live entirely inside a single wrapper file — the wrapper doesn’t need to add an export to the escrowed heist resource (which you couldn’t modify anyway).

Granting rewards

reward.vice and reward.xp from your contract definition are credited automatically on success. You don’t need to call Profile.AddVice or Profile.AddXp yourself for those. reward.cash is not handled by cc_heistcontracts — it’s there for the dashboard’s display only. Cash and inventory payouts are your responsibility:

Cooldowns

Cooldowns are server-wide per contract, not per player. Set cooldown = <seconds> on the contract and call ApplyCooldown from your entry point when the heist starts (or when it ends — your choice). While the cooldown is active, Purchase.Start rejects with 'on_cooldown' for every player. Buying is not blocked — players can still acquire a slot during cooldown, they just can’t start it until the timer ends.

ApplyCooldown export

Stamps the configured cooldown on contractId and schedules the reset callback. Returns the unix-seconds expiry timestamp, or 0 if the contract has no cooldown configured (or the id is unknown). Calling it again before the timer expires re-stamps from “now” — the previously-scheduled reset will silently no-op.

Behaviour notes

  • In-memory only. Cooldowns do not persist across resource restarts.
  • Single timer per contract. reset fires exactly once per cooldown cycle, even if ApplyCooldown is called multiple times during the cycle.
  • No automatic apply. cooldown on the contract definition is just a duration — nothing happens until your entry point calls ApplyCooldown.
  • Surfaced in the rotation payload. Active cooldowns ship to the dashboard as rotation.cooldowns = { [contractId] = expiresAtUnixSeconds }, so the UI can grey out cooldowned cards.

reset vs. cancel

Both are callbacks you can register in opts. They fire on different events and have different jobs: A heist can have both, one, or neither.

Integrating a third-party (escrowed) heist

The same shape works for heists you didn’t write — including ones whose source is escrowed. Create a small wrapper resource (call it myserver_heists) with a single Lua file:
That’s the entire wrapper. No edits to the escrowed heist itself. What if the escrowed heist doesn’t expose an abort? Omit opts.cancel. The Give Up button will be hidden on the dashboard — players have to finish or wait it out. What if the escrowed heist doesn’t fire a completion event? That’s the one case you genuinely can’t wrap around. Players’ slots will get stuck “active” because nothing ever calls FinishHeist. Ask the escrowed heist’s author to add an event. Add cc_heistcontracts and cc_lib to your wrapper’s fxmanifest.lua dependencies so boot order is correct: