<!-- Mercovi · API reference · https://mercovi.com/developers/reference · last reviewed 2026-10-09 -->

_Every Mercovi API v1 endpoint with parameters, samples in eight languages and a Try it button that calls the sandbox._

# API reference

Every endpoint, with its parameters, a sample in eight languages and the sandbox's real answer. Press Try it to send the request yourself.

## Introduction

The Mercovi API is organized around REST. It has resource URLs, takes and returns JSON, and uses standard HTTP methods, status codes and bearer authentication.

The API is in **preview**. The sandbox answers every endpoint on this page with sample data and keeps nothing you send. Production keys are set up with each customer during onboarding — [book a call](/demo?from=developers) to start.

Times are ISO 8601 in UTC. Dates without a time are `YYYY-MM-DD`. Money is an integer in cents, with a lowercase `currency`. IDs are strings with a prefix that names the object, for example `job_` or `man_`.

Base URLs:

```text
Sandbox      https://mercovi.com/api/sandbox/v1
Production   issued with your keys
```

## Authentication

Send your key in the `Authorization` header as a bearer token on every request, over HTTPS. Requests without a key, or with one Mercovi didn't issue, get a `401`.

Sandbox keys begin `mk_test_` and live keys begin `mk_live_`. The sandbox key on these pages is public on purpose: it only opens sample data. Live keys are secrets. Keep them on your server, never in a browser or a mobile app, and revoke any key you think has leaked.

Each live key carries scopes, and an endpoint answers `403` if the key lacks the one it needs. The scopes are `clients:read`, `clients:write`, `sites:read`, `sites:write`, `profiles:read`, `profiles:write`, `jobs:read`, `jobs:write`, `containers:read`, `containers:write`, `manifests:read`, `manifests:write`, `invoices:read`, `webhooks:read`, `webhooks:write`, `events:read`.

Authenticated request:

```sh
curl https://mercovi.com/api/sandbox/v1/clients \
  -H "Authorization: Bearer mk_test_sbx_Ridgeview7Hq2Lk4Wd9"
```

## Errors

Mercovi returns `2xx` when a request worked, `4xx` when something in the request needs to change, and `5xx` when something failed on our side. Every error has the same JSON shape: `type`, a machine-readable `code`, a `message` written for the developer reading the log, the `param` at fault when there is one, and the `request_id` — quote it when you write to us.

| Status | Code | When |
| --- | --- | --- |
| 400 | `parameter_missing` | A required field is missing. `param` names it. |
| 400 | `parameter_invalid` | A field has the wrong type or a value outside its list. |
| 401 | `api_key_missing` | No `Authorization: Bearer` header. |
| 401 | `api_key_invalid` | The key isn't one Mercovi issued, or it was revoked. |
| 403 | `scope_missing` | The key is real but lacks the scope this endpoint needs. |
| 404 | `resource_missing` | No record with that ID in your account. |
| 405 | `method_not_allowed` | The path exists but not with that method. |
| 409 | `idempotency_conflict` | An `Idempotency-Key` was reused with a different body. |
| 422 | `state_invalid` | The record can't make that change from where it is — for example, cancelling a closed job. |
| 429 | `rate_limited` | Too many requests. Wait for the time in `Retry-After`. |
| 500 | `api_error` | Something failed on Mercovi's side. Safe to retry with the same `Idempotency-Key`. |

Error response:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "`kind` is required.",
    "param": "kind",
    "doc_url": "/developers/reference#errors",
    "request_id": "req_7Gx2Sample0001"
  }
}
```

## Pagination

List endpoints return a page of records, newest first unless the endpoint says otherwise, with `has_more` and `next_cursor`. Ask for the next page by passing `next_cursor` as `starting_after`. `limit` sets the page size, from 1 to 100; the default is 25.

Cursors are record IDs, so pages stay stable when records are added while you read.

List response:

```json
{
  "object": "list",
  "data": [
    "…"
  ],
  "has_more": true,
  "next_cursor": "job_6Cp4HaldenNov"
}
```

## Idempotency

Networks fail. To retry a `POST` without creating the same record twice, send an `Idempotency-Key` header with a value unique to that operation — a UUID works. A retry with the same key and the same body returns the first result. The same key with a different body returns a `409`.

`GET`, `PATCH` and `DELETE` are idempotent already and don't need the header.

Retry-safe create:

```sh
curl -X POST https://mercovi.com/api/sandbox/v1/clients \
  -H "Authorization: Bearer mk_test_sbx_Ridgeview7Hq2Lk4Wd9" \
  -H "Idempotency-Key: 5c0a4e9e-1d3b-4a51-9f0e-2b7f3c6d8a10" \
  -H "Content-Type: application/json" \
  -d '{"name": "Fairhaven Township", "kind": "hhw_program"}'
```

## Versioning

The version is in the path: `/v1`. Within a version, Mercovi only makes additive changes — new endpoints, new optional fields, new event types, new values in a list. Write your client to ignore fields it doesn't know.

A change that would break a working client — removing or renaming a field, changing its type — ships as a new version, announced to key holders first, with the old one kept running alongside.

While the API is in preview, fields may still change inside `v1`. Key holders hear about each change before it ships.

## Rate limits

Every response carries `RateLimit-Policy` and, in production, `RateLimit-Remaining` and `RateLimit-Reset`, so a client can slow down before it's stopped. A request over the limit gets a `429` with a `Retry-After` header in seconds.

Limits are set per account. If an integration needs more — a nightly sync of a large history, say — tell us and we'll set the limit with you.

## Webhooks

Register an HTTPS endpoint and Mercovi sends it an `event` object whenever one of the events you chose happens. Answer with any `2xx` within a few seconds and do the work afterwards. Deliveries that fail are retried with growing gaps between attempts. If your endpoint was down, list `/v1/events` to catch up.

Each delivery carries a `Mercovi-Signature` header: `t=<unix time>,v1=<signature>`, where the signature is an HMAC-SHA256 of `<t>.<raw body>` keyed by the endpoint's `whsec_` secret. Compute it yourself, compare in constant time, and reject deliveries whose `t` is more than five minutes old.

| Event | When it's sent |
| --- | --- |
| `job.created` | A job — an HHW event, a C&I pickup or a lab pack — is created. |
| `job.scheduled` | A job gets its date and is staffed. |
| `job.closed` | A job is closed out: containers counted, weights in. |
| `staff.accepted` | A crew member accepts an assignment on a job. |
| `container.received` | A container is checked in at a facility. |
| `container.storage_status_changed` | A container's storage status moves — for example from within the limit to approaching it. |
| `manifest.ready_for_signature` | A manifest is complete and waiting for the generator's signature. |
| `manifest.signed` | A manifest is signed; its status, submission method and tracking number are on the record. |
| `profile.approved` | A waste profile is approved. |
| `profile.expiring` | A waste profile is nearing its renewal date. |
| `invoice.sent` | An invoice is sent to the client. |
| `invoice.paid` | An invoice is paid, in the client portal or recorded by your team. |

Verify a delivery (Node.js):

```js
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Date.now() / 1000 - Number(parts.t);
  if (!(age >= 0 && age < 300)) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  const want = Buffer.from(expected, "hex");
  return given.length === want.length && crypto.timingSafeEqual(given, want);
}
```

## Clients

The organizations you serve: an HHW program, a plant or institution you collect from, or a laboratory. Sites, profiles, jobs and invoices all hang off a client.

The client object: `id` (string) Unique identifier for the client, beginning `cli_`. `object` (string) Always `client`. `name` (string) The client's name as it appears on documents. `kind` (enum) Which line the client is served under. Drives the client portal's navigation. `epa_id` (string) The client's EPA ID number, when it is the generator of record. `billing_email` (string) Where invoices go. `portal_enabled` (boolean) Whether the client can sign in to the client portal. `metadata` (object) Up to 20 key-value pairs of your own, for example the record's ID in your system. Mercovi stores them and never reads them. `created_at` (timestamp) When the record was created, ISO 8601 in UTC. `updated_at` (timestamp) When the record last changed, ISO 8601 in UTC.

### List clients

`GET /v1/clients` — Returns your clients, newest first. Scope: `clients:read`. Parameters: `kind`, `limit`, `starting_after`.

### Create a client

`POST /v1/clients` — Adds a client. Send an `Idempotency-Key` so a retried request can't create it twice. Scope: `clients:write`. Parameters: `name` (required), `kind` (required), `epa_id`, `billing_email`, `portal_enabled`, `metadata`.

### Retrieve a client

`GET /v1/clients/{id}` — Returns one client. Scope: `clients:read`. Parameters: `id` (required).

### Update a client

`PATCH /v1/clients/{id}` — Changes the fields you send and leaves the rest alone. Scope: `clients:write`. Parameters: `id` (required), `name`, `billing_email`, `portal_enabled`, `metadata`.

## Sites

A place you collect from: an event location, a plant's loading dock, a lab's stockroom. A client can have many.

The site object: `id` (string) Unique identifier for the site, beginning `site_`. `object` (string) Always `site`. `client` (string) The client's ID. `name` (string) What your crew calls the place. `address` (object) `line1`, `line2`, `city`, `state`, `postal_code`. `epa_id` (string) The site's own EPA ID, when it has one. `access_notes` (string) Gate codes, dock hours, who to ask for. `metadata` (object) Up to 20 key-value pairs of your own, for example the record's ID in your system. Mercovi stores them and never reads them. `created_at` (timestamp) When the record was created, ISO 8601 in UTC.

### List sites

`GET /v1/sites` — Returns sites, newest first. Scope: `sites:read`. Parameters: `client`, `limit`, `starting_after`.

### Create a site

`POST /v1/sites` — Adds a site to a client. Scope: `sites:write`. Parameters: `client` (required), `name` (required), `address` (required), `access_notes`, `metadata`.

### Retrieve a site

`GET /v1/sites/{id}` — Returns one site. Scope: `sites:read`. Parameters: `id` (required).

### Update a site

`PATCH /v1/sites/{id}` — Changes the fields you send. Scope: `sites:write`. Parameters: `id` (required), `name`, `access_notes`, `metadata`.

## Waste profiles

A characterized waste stream for a client: what it is, its waste codes and DOT description, and whether the receiving facility has approved it. Manifest lines are built from profiles.

The waste_profile object: `id` (string) Unique identifier for the profile, beginning `prof_`. `object` (string) Always `waste_profile`. `client` (string) The client's ID. `name` (string) The stream's common name. `waste_codes` (array) Federal and state waste codes, for example `D001`, `F003`. `dot_description` (string) The proper shipping name, hazard class, UN number and packing group. `status` (enum) Where the profile is in approval. `approved_by` (string) The receiving facility's name, once approved. `created_at` (timestamp) When the record was created, ISO 8601 in UTC. `updated_at` (timestamp) When the record last changed, ISO 8601 in UTC.

### List waste profiles

`GET /v1/waste_profiles` — Returns profiles, newest first. Scope: `profiles:read`. Parameters: `client`, `status`, `limit`, `starting_after`.

### Create a waste profile

`POST /v1/waste_profiles` — Starts a profile in `draft`. Waste codes are checked against the federal and state code library. Scope: `profiles:write`. Parameters: `client` (required), `name` (required), `waste_codes` (required), `dot_description`.

### Retrieve a waste profile

`GET /v1/waste_profiles/{id}` — Returns one profile. Scope: `profiles:read`. Parameters: `id` (required).

### Update a waste profile

`PATCH /v1/waste_profiles/{id}` — Changes a profile in `draft`. An approved profile is changed by creating a new one. Scope: `profiles:write`. Parameters: `id` (required), `name`, `waste_codes`, `dot_description`.

## Jobs

One piece of work: an HHW collection event, a commercial pickup, or a lab pack. A job has a client, a site, a date, a crew and containers; closing it writes the bill.

The job object: `id` (string) Unique identifier for the job, beginning `job_`. `object` (string) Always `job`. `type` (enum) What kind of work it is. `client` (string) The client's ID. `site` (string) The site's ID. `status` (enum) Where the job is. `scheduled_for` (date) The date of the work. `window` (object) `start` and `end`, local time, `HH:MM`. `crew` (array) Assigned staff: `name`, `role`, `accepted`. `container_count` (integer) Containers on the job so far. `invoice` (string) The invoice ID, once the job is billed. `metadata` (object) Up to 20 key-value pairs of your own, for example the record's ID in your system. Mercovi stores them and never reads them. `created_at` (timestamp) When the record was created, ISO 8601 in UTC. `updated_at` (timestamp) When the record last changed, ISO 8601 in UTC.

### List jobs

`GET /v1/jobs` — Returns jobs, soonest first. Scope: `jobs:read`. Parameters: `type`, `status`, `client`, `scheduled_from`, `limit`, `starting_after`.

### Create a job

`POST /v1/jobs` — Creates a job in `draft`. Add a date and window and it moves to `scheduled`. Scope: `jobs:write`. Parameters: `type` (required), `client` (required), `site` (required), `scheduled_for`, `window`, `metadata`.

### Retrieve a job

`GET /v1/jobs/{id}` — Returns one job with its crew. Scope: `jobs:read`. Parameters: `id` (required).

### Update a job

`PATCH /v1/jobs/{id}` — Reschedules or annotates a job that isn't closed. Scope: `jobs:write`. Parameters: `id` (required), `scheduled_for`, `window`, `metadata`.

### Cancel a job

`POST /v1/jobs/{id}/cancel` — Cancels a job that hasn't started. The crew is told; nothing is billed. Scope: `jobs:write`. Parameters: `id` (required), `reason`.

## Containers

A drum, tote, box or cart on a job, from the moment it's opened until it ships. At a facility it carries a storage status: where it stands against the limit that applies to that container.

The container object: `id` (string) Unique identifier for the container, beginning `cont_`. `object` (string) Always `container`. `job` (string) The job's ID. `profile` (string) The waste profile in it. `kind` (string) Type and size, for example `55-gal steel drum`. `status` (enum) Where the container is. `weight_lb` (integer) Net weight in pounds, once weighed. `storage_status` (enum) At a facility: where the container stands against the limit that applies to it. Null before receipt. `received_at` (timestamp) When it was checked in at a facility. `created_at` (timestamp) When the record was created, ISO 8601 in UTC.

### List containers

`GET /v1/containers` — Returns containers, newest first. Scope: `containers:read`. Parameters: `job`, `status`, `storage_status`, `limit`, `starting_after`.

### Retrieve a container

`GET /v1/containers/{id}` — Returns one container. Scope: `containers:read`. Parameters: `id` (required).

### Update a container

`PATCH /v1/containers/{id}` — Records a weight or moves the container to its next status. Scope: `containers:write`. Parameters: `id` (required), `status`, `weight_lb`.

## Manifests

A hazardous waste manifest built from a job's containers and profiles. Each manifest carries its EPA e-Manifest status, submission method and tracking number on the record. In the sandbox, tracking numbers are samples.

The manifest object: `id` (string) Unique identifier for the manifest, beginning `man_`. `object` (string) Always `manifest`. `job` (string) The job's ID. `manifest_tracking_number` (string) The manifest tracking number: nine digits and three letters. `status` (enum) Where the manifest is. `submission_type` (enum) The EPA e-Manifest submission method. `generator` (object) `name` and `epa_id`. `designated_facility` (object) `name` and `epa_id`. `lines` (array) One per waste: `profile`, `dot_description`, `waste_codes`, `containers`, `quantity`, `unit`. `created_at` (timestamp) When the record was created, ISO 8601 in UTC. `updated_at` (timestamp) When the record last changed, ISO 8601 in UTC.

### List manifests

`GET /v1/manifests` — Returns manifests, newest first. Scope: `manifests:read`. Parameters: `job`, `status`, `limit`, `starting_after`.

### Create a manifest

`POST /v1/manifests` — Builds a draft manifest from a job: one line per profile, waste codes from the contents. Scope: `manifests:write`. Parameters: `job` (required), `designated_facility` (required), `submission_type`.

### Retrieve a manifest

`GET /v1/manifests/{id}` — Returns one manifest with its lines. Scope: `manifests:read`. Parameters: `id` (required).

## Invoices

The bill a closed job writes. Amounts are integers in cents.

The invoice object: `id` (string) Unique identifier for the invoice, beginning `inv_`. `object` (string) Always `invoice`. `number` (string) The invoice number on the document. `client` (string) The client's ID. `jobs` (array) The job IDs it bills. `status` (enum) Where the invoice is. `currency` (string) Three-letter ISO code, lowercase. `amount_due` (integer) Total in cents. `amount_paid` (integer) Paid so far, in cents. `due_date` (date) When payment is due. `created_at` (timestamp) When the record was created, ISO 8601 in UTC.

### List invoices

`GET /v1/invoices` — Returns invoices, newest first. Scope: `invoices:read`. Parameters: `client`, `status`, `limit`, `starting_after`.

### Retrieve an invoice

`GET /v1/invoices/{id}` — Returns one invoice. Scope: `invoices:read`. Parameters: `id` (required).

## Webhook endpoints

A URL of yours that Mercovi calls when something happens. Each delivery is signed so you can check it came from Mercovi.

The webhook_endpoint object: `id` (string) Unique identifier for the endpoint, beginning `we_`. `object` (string) Always `webhook_endpoint`. `url` (string) An HTTPS URL. `events` (array) Event types to send, or `["*"]` for all. `secret` (string) The signing secret, beginning `whsec_`. Returned once, on create. `enabled` (boolean) Whether deliveries are on. `created_at` (timestamp) When the record was created, ISO 8601 in UTC.

### List webhook endpoints

`GET /v1/webhook_endpoints` — Returns your endpoints. Scope: `webhooks:read`. Parameters: `limit`, `starting_after`.

### Create a webhook endpoint

`POST /v1/webhook_endpoints` — Registers a URL. The response carries the signing secret — store it; it isn't shown again. Scope: `webhooks:write`. Parameters: `url` (required), `events` (required).

### Delete a webhook endpoint

`DELETE /v1/webhook_endpoints/{id}` — Stops deliveries and removes the endpoint. Scope: `webhooks:write`. Parameters: `id` (required).

## Events

A record of something that happened. The same object is what a webhook delivers, and you can list recent events to catch up after downtime.

The event object: `id` (string) Unique identifier for the event, beginning `evt_`. `object` (string) Always `event`. `type` (enum) What happened. `data` (object) `object`: the record as it was right after the change. `created_at` (timestamp) When the record was created, ISO 8601 in UTC.

### List events

`GET /v1/events` — Returns recent events, newest first. Scope: `events:read`. Parameters: `type`, `limit`, `starting_after`.

### Retrieve an event

`GET /v1/events/{id}` — Returns one event. Scope: `events:read`. Parameters: `id` (required).
