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. The same value reaches you three ways, and any of them can be used in these paths:

Where you read itField
Get crypto depositsdeposit_id
Deposit status update webhookdeposit_id
Get depositsmovement_id

There is no new identifier to learn, and no lookup to perform to translate between them.

Which one you reach for depends on how you already track deposits. If you consume the webhook, take deposit_id straight from the payload. If you reconcile from settlement, the same value is the movement_id on Get deposits.

📘

Submit once the deposit has settled

The identifier exists from the moment a deposit is detected, so you can call these endpoints before the deposit is credited. Doing so is accepted rather than rejected, but the submission is then held while the deposit works through confirmations and compliance review, and a held submission gives up if the deposit takes too long, leaving the record failed.

The reliable point to submit is once the deposit has been credited, which is when it appears on Get deposits. Whenever you submit ahead of that, confirm the outcome with a GET and resubmit under a new request_id if the record did not reach reported.

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.


Choosing an effective_datetime

effective_datetime controls when the cost basis you submit starts applying to the
account. Basis is applied from that timestamp forward, so prior tax periods are not
retroactively recalculated.

If you omit effective_datetime, the basis applies from the deposit's own
transaction datetime.

ScenarioRecommended effective_datetime
Deposit occurred in the current tax yearOmit the field, or set it to the deposit datetime
Deposit occurred in a prior tax year, basis submitted nowJanuary 1 of the current tax year
Deposit occurred in a prior tax year, basis submitted early in the new year before forms are finalizedJanuary 1 of the prior tax year
📘

Constraint

effective_datetime must be at or after the deposit's transaction datetime. An
earlier timestamp is rejected.

Setting the effective datetime to January 1 of the current tax year is the common
default for backfilling prior-year deposits: it keeps the corrected basis aligned with
the tax year in which it was provided, rather than reopening a year that has already
been reported. If you are submitting basis early in a new year and the prior year's
forms have not yet been finalized, setting January 1 of the prior year lets the
correction land on those forms instead.

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, provided that happens within the hold window described below.

Submitting early is not an error, and in the common case it needs no handling. A held submission is retried on the zerohash side on a backoff while it waits for the deposit to report, but the wait is not open-ended: if the deposit has not reported within roughly half an hour, the record lands failed and the basis must be submitted again under a new request_id. Once a submission is past the hold stage and in flight to the tax subsystem, it is retried for up to 24 hours. 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?