Report Cost Basis On Deposits

Attach cost basis to a customer's crypto deposit so their gains are reported correctly on Form 1099-DA

Why cost basis matters on a deposit

When a customer deposits crypto onto your platform from an external wallet, zerohash reports the deposit for tax purposes but has no visibility into what the customer paid for the asset or when they acquired it. The deposit is therefore treated as having unknown, effectively zero, cost basis. When the customer later sells, the entire proceeds look like gain, and the 1099-DA overstates what they owe.

If you already hold that acquisition history, these endpoints let you hand it over so the sale is reported against the real basis.

When to use this

  • Migrating an existing book of business to zerohash. You hold the full acquisition history for balances you are transferring in.
  • Supporting external-wallet deposits. Your customer acquired the asset elsewhere and you know the acquisition price and date.

This is opt-in. If you do not submit basis, deposits continue to be reported as they are today.

Endpoints

VerbPathPurpose
POST/deposits/{deposit_id}/lotsSubmit one or more cost-basis lots for a deposit
GET/deposits/{deposit_id}/lotsRead the lots currently on file, and their status
DELETE/deposits/{deposit_id}/lotsRemove the lots so basis can be corrected

Finding the deposit_id

deposit_id is an identifier you already have. It is the deposit_id returned by the GET crypto deposits endpoints, and it is the same value as the movement_id returned by the GET deposits endpoint. There is no new identifier to learn.

Submit cost basis

Send one lot per acquisition. A deposit that was accumulated over several purchases takes several lots in a single request.

FieldRequiredDescription
lotsYesArray of one or more cost-basis lots.
request_idNoYour idempotency key for the create. zerohash generates a UUIDv4 when you omit it and echoes it in the response. Replaying the same request_id returns the existing record instead of creating a second one.
effective_datetimeNoRFC3339 timestamp marking when the lots take effect for tax reporting. When omitted, no value is sent and the tax subsystem applies its own default.

Each entry in lots:

FieldRequiredDescription
quantityYesQuantity acquired in this lot, in units of the deposited asset.
cost_basisYesTotal cost basis of the lot in USD.
acquisition_transaction_datetimeNoRFC3339 timestamp of the original acquisition. When omitted, no value is sent and the tax subsystem applies its own default.

Example POST /deposits/{deposit_id}/lots request:

{
  "request_id": "21bdbb11-712f-4cb7-a178-4f277c80cde0",
  "lots": [
    {
      "quantity": "1.5",
      "cost_basis": "45000.00",
      "acquisition_transaction_datetime": "2024-01-01T00:00:00Z"
    },
    {
      "quantity": "0.5",
      "cost_basis": "21000.00",
      "acquisition_transaction_datetime": "2024-06-14T09:30:00Z"
    }
  ]
}

Example 201 response:

{
  "message": {
    "request_id": "21bdbb11-712f-4cb7-a178-4f277c80cde0",
    "deposit_id": "21bdbb11-712f-4cb7-a178-4f277c80cde0",
    "status": "reported",
    "participant_code": "ABCDEF",
    "account_label": "general",
    "effective_datetime": "2024-01-01T00:00:00Z",
    "lots": [
      {
        "quantity": "1.5",
        "cost_basis": "45000.00",
        "acquisition_transaction_datetime": "2024-01-01T00:00:00Z"
      }
    ],
    "created_at": "2024-01-01T00:00:00Z",
    "updated_at": "2024-01-01T00:00:00Z"
  }
}
❗️

A 201 means the submission was accepted, not that the basis was applied. The most common failure, lot quantities that do not sum to the deposit amount, still returns 201 and then lands the record in a failed state. Always confirm with a GET before treating basis as recorded.

You do not need to wait for the deposit to report

Submit basis whenever you have it. You do not have to time the request against zerohash's own reporting cycle.

  • If the deposit has already been reported, the usual case, the lot is forwarded in the same call. The response comes back reported when the tax subsystem accepts it, or failed when it rejects it.
  • If the deposit has not been reported yet, zerohash holds the submission and forwards it automatically once the deposit reports.

Submitting early is not an error you need to handle or retry. A held submission is retried on the zerohash side on a backoff until the deposit reports, for up to 24 hours from creation. Deletes are not queued: they are processed within the request.

Check what is on file

GET /deposits/{deposit_id}/lots returns the current record for the deposit, including status and, when something went wrong, error_message.

StatusMeaning
pending_transactionHeld. The deposit has not been reported yet, so the lots are waiting on it.
readyThe deposit is resolved and the lots are waiting to be submitted.
pendingSubmission is in flight.
reportedAccepted by the tax subsystem and used for gain/loss calculation. Terminal.
failedThe last attempt failed. See below: this is not always final.
deletedThe lots were removed. Terminal, and the request_id is spent.

A failed record is normally retried automatically, on a backoff, for up to 10 attempts or 24 hours from creation. A failure the tax subsystem will never accept, a quantity mismatch for example, stops immediately instead. error_message tells the two apart: it is prefixed NON_RETRYABLE: when the submission was rejected outright, and MAX_RETRIES_EXCEEDED: when the retry budget ran out. In both of those cases the record is final, and correcting it means submitting again under a new request_id.

Correct or replace basis

A deposit carries one active lot record at a time. To change basis, delete the existing record, then submit the corrected lots under a new request_id. A deleted request_id is spent: reusing it returns 409.

DELETE /deposits/{deposit_id}/lots removes the lots recorded for the deposit. A delete is refused while a submission is in flight, so retry shortly if that happens.

📘

A second POST under a different request_id, while an active record is on file, returns 409. Delete the existing record first, then resubmit under a new request_id.


Rules and error responses

  • Quantities must sum to the deposit amount. A mismatch does not return an HTTP error. The record lands failed with a NON_RETRYABLE: prefix on error_message, and is not retried.
  • Digital assets only. A deposit of a fiat asset is rejected with 422.
  • One active record per deposit. A create under a different request_id returns 409 while an active record exists. Deleted and permanently failed records do not count as active.
  • request_id is optional. zerohash generates a UUIDv4 when you omit it and returns it in the response. Replaying it returns the existing record; once that record is deleted or has permanently failed, the key is spent and returns 409.
CodeMeaning
201Submission accepted, or an idempotent replay of an existing record. Confirm the outcome with a GET.
400Validation failure. errors lists each problem.
403Authentication or permission failure, including a key without write access.
404The deposit does not exist, is not visible to your platform, or has no lots.
409This deposit already has an active lot record, or the request_id belongs to a record that has been deleted or has permanently failed.
422Transfer lots are not supported for this deposit's asset, for example fiat. POST only.
500Unexpected server error.
502Upstream error, including a delete attempted while a submission is in flight. Retry shortly.
503A dependency was unavailable. Safe to retry; the response carries zh-allow-retry: true.

Did this page help you?