Skip to content

Receiving Transfers

Creating Transfers covers the shipper's side. This page covers everything after the manifest exists: the recipient accepting or refusing it, the transporter moving it, and the notes both sides leave along the way.

Step Endpoint Who does it
Approve POST /v2/transfers/approve Recipient
Reject POST /v2/transfers/reject Recipient
Receive POST /v2/transfers/receive Recipient
Take back a rejection POST /v2/transfers/rejected/receive Shipper
Crew the return leg POST /v2/transfers/transporters/update Shipper
Accept · Depart · Check-in · Check-out · Arrive POST /v2/transfers/hub/{action} Transporter
Correct transport details POST /v2/transfers/hub/update Transporter
Add a note POST /v2/transfers/notes/create Either
Void a note POST /v2/transfers/notes/void Author

Like every mutation in this API, these are dry runs by default. Add submit=true to actually write to Metrc — see Getting Started.

import requests

API = "https://api.trackandtrace.tools"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
PARAMS = {"licenseNumber": "MAN000030"}

The two IDs, and why transfers are confusing

Almost every mistake on this page is the wrong ID. A transfer is three nested things:

Transfer  (id)              — the manifest
  └─ Delivery  (deliveryId) — one leg to one recipient
       └─ Package (id)      — one package on that delivery

A transfer with one destination still has a delivery, and its deliveryId is usually not the same number as the transfer's id. Endpoints on this page want:

  • id — the transfer, from /v2/transfers/incoming/active or /v2/transfers/rejected
  • shipmentDeliveryId — the delivery, from /v2/transfers/deliveries
  • package id — from /v2/transfers/packages, not from /v2/packages

The hub endpoints are the exception: they take deliveryId directly, read from the deliveryId field of /v2/transfers/hub.

Package IDs are delivery-scoped

The id on a package in /v2/transfers/packages identifies that package on that delivery. It is a different number from the package's own ID in /v2/packages, and sending the latter is the most common cause of a rejected receive payload.

Approving, before you can receive

Some states put an approval step in front of receiving: the transfer arrives pending approval and cannot be received until the recipient approves it. States that do not use the step never produce transfers in it, and you can skip straight to Receive.

requests.post(
    f"{API}/v2/transfers/approve",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json=[{"id": 12345, "shipmentDeliveryId": 6789}],
)

Set autoApprove to approve later transfers from the same shipper automatically — but only on a transfer whose record allows it, or Metrc refuses the whole call.

Refusing the whole transfer

reasons = requests.get(
    f"{API}/v2/transfers/reject/inputs", params=PARAMS, headers=HEADERS
).json()["actionReasons"]

reason = next(r for r in reasons if r["forRejectedTransferApprovals"])

requests.post(
    f"{API}/v2/transfers/reject",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json=[{
        "id": 12345,
        "shipmentDeliveryId": 6789,
        "rejectReasonId": reason["id"],
        "rejectReasonNote": "Manifest does not match the delivery",
    }],
)

rejectReasonNote is required whatever reason you pick — unlike elsewhere, where the reason's own requiresNote flag decides.

To refuse only some packages, do not use this endpoint. Receive the transfer and mark those packages rejected instead; see Rejecting individual packages.

Receiving

Receiving is one call that books every package on the delivery. A package left out of packages is not left untouched — Metrc receives the delivery as a whole, so omitting one is not a way to defer it.

Step 0 — fetch the inputs

inputs = requests.get(
    f"{API}/v2/transfers/receive/inputs", params=PARAMS, headers=HEADERS
).json()

locations = inputs["locations"]
units = inputs["unitsOfMeasure"]

Step 1 — read what is on the delivery

packages = requests.get(
    f"{API}/v2/transfers/packages",
    params={**PARAMS, "deliveryId": 6789},
    headers=HEADERS,
).json()["data"]

Step 2 — receive it

requests.post(
    f"{API}/v2/transfers/receive",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json=[{
        "id": 12345,
        "shipmentDeliveryId": 6789,
        "packages": [
            {
                "id": p["id"],
                "receivedQuantity": p["shippedQuantity"],
                "receivedUnitOfMeasureId": p["shippedUnitOfMeasureId"],
                "locationId": locations[0]["id"],
            }
            for p in packages
        ],
    }],
)

Receiving the shipped quantity unchanged is the simple case. Two things complicate it.

Variances

If receivedQuantity differs from what was shipped, that is a variance and Metrc refuses it unless you say so:

{"id": 222333, "receivedQuantity": 9.0, "receivedUnitOfMeasureId": 1,
 "locationId": 505, "confirmVariance": True}

Rejecting individual packages

A rejected package needs a reason, and locationId / receivedQuantity stop being required for it — it is not entering your inventory.

reason = next(
    r for r in inputs["actionReasons"] if r["forRejectedTransferPackages"]
)

{"id": 222333, "rejected": True, "rejectedReasonId": reason["id"],
 "reasonNote": "Seal broken in transit"}

reasonNote is required when the reason you picked has requiresNote: true. Check the flag rather than always sending one — an unexpected note is as rejectable as a missing one.

Fields that vary by state

Field Sent in
locationId everywhere except California
sublocationId Maine and Michigan only
rIdQrScanned California and Michigan only

T3 strips the fields your state does not use, so sending locationId from a California session is harmless. It does not invent the ones your state requires.

wholesalePrice

Required per-package only when the transfer type has transactionType="Wholesale" — see Choosing a transfer type.

Taking back a rejection

When a recipient rejects packages, they travel back and the shipper books them in. This is a different endpoint from receiving, with different field names:

requests.post(
    f"{API}/v2/transfers/rejected/receive",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json=[{
        "id": 12345,
        "shipmentDeliveryId": 6789,
        "packages": [{
            "id": 222333,
            "locationId": 505,
            "receiverRejectedReasonId": 9,
            "receiverReasonNote": "Seal broken in transit",
        }],
    }],
)

receiverRejectedReasonId, not rejectedReasonId

The two forms look alike and Metrc even gives them the same HTML form ID, but the return form uses receiverRejectedReasonId and receiverReasonNote. A payload built for /v2/transfers/receive will not work here. There is also no receivedQuantity, receivedUnitOfMeasureId, confirmVariance or wholesalePrice — nothing is being accepted, only booked back in.

The return trip needs its own transporters, set with POST /v2/transfers/transporters/update. Its payload mirrors the transporters block of /v2/transfers/update.

The transporter's view — the hub

A transporter carrying someone else's manifest works from the hub, not the transfer listing. Each leg moves through five states, each its own endpoint, all taking the same two fields:

delivery = requests.get(f"{API}/v2/transfers/hub", params=PARAMS, headers=HEADERS).json()["data"][0]

body = {
    "deliveryId": delivery["deliveryId"],
    "direction": delivery["transporterDirectionName"],   # "Outbound" or "Return"
}

for action in ["accept", "depart", "check-in", "check-out", "arrive"]:
    requests.post(
        f"{API}/v2/transfers/hub/{action}",
        params={**PARAMS, "submit": "true"},
        headers=HEADERS,
        json=body,
    )

That loop is illustrative, not a recipe — each transition has preconditions, and Metrc rejects one taken out of order:

Action Meaning Requires
accept The transporter takes the job
depart Left the origin accepted
check-in Arrived at a layover departed
check-out Left a layover checked in
arrive Reached the destination departed

check-in and check-out only apply to a layover. A direct run is acceptdepartarrive.

Metrc's UI chains accept and depart

Pressing Depart in Metrc on a delivery that has not been accepted quietly sends accept and then depart. The T3 endpoints are the primitives, so if you want that behaviour, send both.

Correcting transport details

Route, times, drivers and vehicles on a delivery already in flight:

requests.post(
    f"{API}/v2/transfers/hub/update",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json=[{
        "id": delivery["id"],
        "shipmentDeliveryId": delivery["deliveryId"],
        "transporterDirection": "Outbound",
        "estimatedDepartureDateTime": "2026-05-23T21:22:35",
        "estimatedArrivalDateTime": "2026-05-24T09:00:00",
        "plannedRoute": "I-70 west to Columbia, then US-63 north",
        "transporterDetails": [{
            "driverName": "Jane Doe",
            "driverOccupationalLicenseNumber": "LIC12345",
            "driverLicenseNumber": "D1234567",
            "vehicleMake": "Ford",
            "vehicleModel": "Transit",
            "vehicleLicensePlateNumber": "ABC1234",
        }],
    }],
)

This cannot change what is being shipped — use /v2/transfers/update for that. Saved drivers and vehicles are available from /v2/transfers/create/inputs, but every field here is free text, so a driver who is not on file can still be entered.

Notes

A note is attached to a transfer, visible to both parties, and cannot be edited — correcting one means voiding it and adding a replacement.

reasons = requests.get(
    f"{API}/v2/transfers/notes/inputs", params=PARAMS, headers=HEADERS
).json()["actionReasons"]

reason = next(r for r in reasons if r["forTransferNotes"])

requests.post(
    f"{API}/v2/transfers/notes/create",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json=[{
        "shipmentPlanId": 12345,
        "reasonId": reason["id"],
        "note": "Driver rerouted around a road closure on I-70",
    }],
)

shipmentPlanId is the transfer's own id. Voiding takes the note's ID:

requests.post(
    f"{API}/v2/transfers/notes/void",
    params={**PARAMS, "submit": "true"},
    headers=HEADERS,
    json={"id": 4321},
)

Next Steps