Skip to navigation

Void a signal (Early access)

A void takes one signal out of your usage. Use it when a signal should never have counted: a test event sent to production, a signal sent for the wrong customer, or a retry your system sent under a second key.

Early access. Signal voiding is new and still being extended.

A voided signal stops counting as usage in every billing period it was counted in. Paid keeps the signal. By default the signal reads leave it out, and you can read it with includeVoided=true.

A void is permanent. There is no way to un-void a signal. To correct a signal, void it and send a new one under a new idempotency key.

Prerequisites

  • An organization API key with the write:usage scope. Full-access keys have it. Voiding needs an API key; a dashboard session cannot void a signal. A void is not available through the MCP server or the in-product assistant yet.
  • Signal voiding turned on for your organization. Ask your Paid contact to enable it. Until then, the void endpoints answer 404, as for a path that does not exist. That 404 carries API_ENDPOINT_NOT_FOUND under error.code. A missing signal answers 404 with SIGNAL_NOT_FOUND in a top-level code instead. While voiding is being turned on for you, a request can briefly get 403 with FEATURE_NOT_ENABLED instead of the 404.
  • The signal’s ID, or the idempotency key you sent it with. Every signal in GET /api/v2/signals carries its id.

Void a signal by ID

PUT /api/v2/signals/{id}/void voids the signal with that ID. The body is optional. Send a reason of up to 500 characters if you want one kept with the void.

curl -X PUT https://api.agentpaid.io/api/v2/signals/6890b0e2a6f2c30012f0a1b3/void \
-H "Authorization: Bearer $PAID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Test event sent to production"}'
{
"signalCorrectionId": "0f6c2f4e-8d0b-4c1a-9a53-6a7d3a1b2c90",
"alreadyVoided": false,
"factsReverted": 1,
"pendingCancelled": 0,
"cells": [
{
"orderLineAttributeId": "4b7a1d3e-52c9-4e0f-8f21-9c3d6e5a7b18",
"nextBillingDate": "2026-12-01T00:00:00.000Z",
"quantity": 1,
"nonBillable": false
}
],
"credits": []
}
FieldMeaning
signalCorrectionIdThe ID of the void itself.
alreadyVoidedtrue when the signal had been voided before and nothing changed now.
factsRevertedHow many of the signal’s usage records the void took back: one for each entry in cells. 0 when the signal was not counted in any billing period. On a repeat of an earlier void this is that void’s count, and cells is empty.
pendingCancelledHow many of the signal’s credit charges that had not yet been applied the void cancelled. 0 when there were none. On a repeat of an earlier void this is that void’s count.
cellsThe usage the void took back, one entry per order attribute and billing period the signal was counted in, and a second entry for the same period when it also held tracked-only usage (nonBillable is true). Empty when the signal was not counted anywhere, and on a repeat of an earlier void.
cells[].orderLineAttributeIdThe ID of the order attribute (an order line’s priced item) whose usage this void took back.
cells[].nextBillingDateThe billing date of the billing period this usage was counted in, as an RFC 3339 timestamp in UTC. With orderLineAttributeId it identifies the period.
cells[].quantityHow much usage was taken back from this billing period, in the attribute’s own usage units: the signal’s net quantity there.
cells[].nonBillabletrue when the usage taken back was tracked but never billed, because the signal’s customer unit is not billable. false for billable usage.
creditsOne entry for each credit currency the signal spent. Empty when it spent none. See Prepaid credits.
credits[].creditsCurrencyIdThe credit currency.
credits[].restoredThe credits the void gave back, as an exact decimal string: credits returned to the customer’s balance, plus any overage the void cleared (usage beyond the customer’s credits, which would have been charged).
credits[].forfeitedThe credits the void could not return, as an exact decimal string, for example because the credits they were spent from have expired or were used up.
credits[].reversalsHow many of the signal’s credit spends the void reversed.

Voiding twice is safe

Voiding a signal that is already voided answers 200 with the same signalCorrectionId and alreadyVoided: true. Your usage, billing and credits are not changed a second time, so you can repeat a void after a timeout without checking first. A voided signal reads as not found by default, but voiding it again still answers 200. Your audit log records the repeat. The repeat’s cells array is empty, because the billing effects were returned when the void was first made.

Void a signal by idempotency key

PUT /api/v2/signals/idempotency-key/{idempotencyKey}/void voids the signal you sent with that idempotency key. The answer is the same as for a void by ID.

  • The key is matched exactly as stored. URL-encode it, in particular if it contains /, ? or #.
  • A signal sent with a customer API key is stored under a namespaced key. Void it by its ID.
  • A key that names no signal answers 404 with SIGNAL_NOT_FOUND. A signal that arrived moments ago may not be stored yet, so try again shortly.
  • A key that names more than one signal answers 409 with AMBIGUOUS_IDEMPOTENCY_KEY and voids nothing. Void the signal you mean by its ID.
  • A key of . or .. cannot be used in the path. An HTTP client resolves those two segments before it sends the request, so the request never names the key. Void that signal by its ID.

After a void

WhatWhat happens
Usage and billingThe signal no longer counts in any billing period it was counted in.
Reading signalsThe three signal reads leave a voided signal out by default. See Reading voided signals.
Sending the signal againA signal sent again under the key of a voided signal is not stored. POST /api/v2/signals/bulk counts it in voided, never in duplicates. A key stays tied to its signal for good, voided or not. To send a corrected signal, use a new idempotency key. See Resending a voided signal.
Prepaid creditsThe credits the signal spent are returned to the customer’s balance where possible. Credits that cannot be returned are forfeited. See below.

Reading voided signals

A voided signal is one you withdrew, so the three signal reads leave it out by default:

ReadA voided signal by default
GET /api/v2/signalsLeft out of the list. pagination.total counts only the signals returned.
GET /api/v2/signals/{id}Answered as an unknown ID: 404 with SIGNAL_NOT_FOUND.
POST /api/v2/signals/lookupListed under notFound, with the IDs Paid cannot find.

No signal in these default responses carries a voided field, because none returned is voided.

Pass includeVoided=true on any of the three reads to include voided signals. Every signal returned then carries voided: true for a voided signal and false for any other. includeVoided=false and no value behave the same.

These reads apply to organizations with signal voiding turned on. For other organizations, nothing is filtered, no signal carries voided, and includeVoided is accepted and ignored.

When signal voiding is on for your organization and Paid cannot read your voided signals, a read answers an error instead of showing a voided signal as live: 503 for the list, and 500 for the read by ID and the lookup. Try again. If Paid cannot tell whether signal voiding is on for your organization, a read behaves as if it were off: it leaves nothing out, and no signal carries voided. The list answers 422 when your organization has more voided signals than one request can leave out. Set includeVoided=true or narrow createdAtFrom.

Resending a voided signal

A voided signal keeps its idempotency key, so a signal you resend under that key is not stored. The bulk ingest response counts it apart from duplicates:

{ "ingested": 3, "duplicates": 1, "failed": 0, "voided": 1 }

This response comes from a batch of 3 new signals, 1 resend of an earlier signal and 1 resend of a voided signal. voided is 0 when none apply. The four counts add up to the number of signals in the batch, so add voided if you reconcile the counts.

Prepaid credits

A signal can spend a customer’s prepaid credits. Voiding it gives them back:

  • The ledger gets a reversal: a new entry that returns the credits to the pool they were spent from. The original spend is never edited. The reversal is recorded when you void, and it counts in the period of the original spend.
  • If that pool can no longer take the credits back, for example because its period has ended, they are forfeited and read as expired. The forfeited amount in the answer says how much.
  • Two cases are refused instead. See CREDIT_POOL_VOIDED and CREDIT_POOL_AMENDED below.

When a void is refused

A refused void changes nothing in your usage, billing or credits. The one exception is a 502 or 503 from a service failure, where the void may or may not have been made: see the last rows of the table. The answer carries a code and an error message. For a refusal about one billing period, details names the order line and the period.

{
"error": "the signal belongs to workflow \"run_8841\" of meter \"Research run\", which has closed; the workflow is charged once it closes, so a void now would not reach the charge; once the period is invoiced, a credit note corrects it",
"code": "WORKFLOW_CLOSED"
}
CodeStatusWhenWhat to do
WORKFLOW_CLOSED400The signal is a step of a workflow that has already closed. Paid charges a workflow once, when it closes, so a void now would not reach the charge. This can also be the answer shortly before the charge is written.Once the period is invoiced, correct the charge with a credit note.
PERIOD_CLOSED400The billing period the signal was counted in is settled.Correct it with a credit note.
VOID_LIMIT_EXCEEDED400The customer has reached the limit on how many signals can be voided in a rolling time window. By default the limit is 100,000 voided signals per customer in any 30 days. It is a per-organization setting, and the error message names the limit that applies. This is not the API rate limit. Retrying right away does not help; the void is allowed again once earlier voids age out of the window.Ask your Paid contact about a larger correction.
COST_PLUS_LINE400The signal was billed on a cost-plus price, which a void does not yet reach.Correct it with a credit note.
CREDIT_POOL_VOIDED400A credit the signal spent sits in a pool that was voided with its order, or with an invoice.Reactivate the order, then void the signal.
CREDIT_POOL_AMENDED400A credit the signal spent sits in a pool that an order change replaced, and the credits cannot yet follow the usage to the new pool.Correct the customer with a credit note or a grant.
COVERED_BY_ACTIVATION_CHARGE400The signal added to or took away from usage that an order’s activation already charged in prepaid credits. That charge covers all the usage of the billing period so far as a single charge that names no signal, so the credits it spent cannot be given back for one signal. Nothing is voided and nothing is returned, including any other charge the signal has. details names the order line and the period.Correct the customer with a credit grant or a credit note. See Signals counted by an activation charge.
SIGNAL_NOT_FOUND404No signal in your organization has that ID or key.Check the ID or key. If the signal arrived moments ago, try again shortly.
AMBIGUOUS_IDEMPOTENCY_KEY409The key names more than one signal.Void the signal you mean by its ID.
SIGNAL_NOT_INDEXED, RETRY_LATER400The signal is not ready to be voided yet, or cannot be voided right now.Try again in a few minutes.
SIGNAL_HAS_NO_CUSTOMER400The signal has no customer, so there is nothing to void.None needed.
INVALID_REQUEST400The ID or key in the path has a malformed percent-escape.Percent-encode the key properly.
VALIDATION_ERROR400The request is not valid: an ID that is not a signal’s 24-character hex ID, or a reason of more than 500 characters.Fix the ID or the body and send it again.
UNAUTHORIZED401The request has no valid API key.Send a valid API key.
INSUFFICIENT_SCOPE403The API key does not have the write:usage scope. details names the scope.Use a key with write:usage.
FEATURE_NOT_ENABLED403Signal voiding is not turned on for your organization yet, as seen while it is being enabled.Try again shortly, or ask your Paid contact.
SIGNAL_ORGANIZATION_MISMATCH, SIGNAL_CUSTOMER_MISMATCH409The signal’s billing records belong to a different organization or customer than the request. Nothing is voided.Ask your Paid contact.
CUSTOMER_KEY_FORBIDDEN403The request used a customer API key, which is scoped to one customer and cannot call this endpoint.Use an organization API key.
API_KEY_REQUIRED403The request did not use an API key, for example a dashboard session.Send an API key.
SIGNAL_LOOKUP_FAILED502Paid could not read the signal, so it sent no void. An earlier void of the signal may already have gone through.Send the same request again.
BILLING_SERVICE_UNAVAILABLE502, 503Paid could not finish the void. It may or may not have been made.Send the same request again. A repeat is safe and tells you which. If it keeps failing, ask your Paid contact.
RATE_LIMIT_EXCEEDED429You have hit the API rate limit.Wait the number of seconds in the Retry-After header, then retry.

Signals counted by an activation charge

When an order is activated, a usage line priced in prepaid credits from a meter charges the usage that came in before activation once. The charge is a single spend that names no signal. A void of one of those signals is refused with COVERED_BY_ACTIVATION_CHARGE, because there is no spend of that signal’s to give back.

For example, a customer’s order is activated on 15 November, and its billing period runs 1 to 30 November. A usage line on it is priced in prepaid credits from a meter that sums tokens. A signal dated 10 November with 500 tokens came in before activation. At activation, Paid charged the customer’s credits once for all the usage of that period so far.

  • Voiding the 10 November signal is refused. Nothing is voided and no credits are returned. To correct the customer, grant credits for the 500 tokens or issue a credit note.
  • A signal dated 20 November, sent after activation, is charged on its own and voids as usual. One sent within about a minute of the activation charge may still be refused.
  • A signal dated in October, before the period the charge covered, voids as usual.
  • If the order has been deleted, a void of such a signal is accepted and returns no credits.

Settled and invoiced periods

Once a billing period is settled, no signal counted in it can be voided. A period settles when Paid closes it for invoicing. After that, the correction is a credit note, never a void.

The period that decides is the one the signal was billed in, not the date on the signal. Usage that arrived late and was carried forward into an open period is still voidable there.

A line priced from a meter that bills once per period is the exception. This covers any such meter: count, sum, maximum, minimum, average, latest value or unique count. For these lines, the period that holds the signal’s own time also decides, whether or not that meter counts the signal. If the customer has such a line and that period is settled, the void is refused with PERIOD_CLOSED.

Voiding signals that feed a workflow

A workflow groups the signals of one run, such as the steps of one agent task. A workflow meter charges it once, when it closes, by writing one charge signal.

  • A step of an open workflow. An open workflow adds no refusal of its own. The other checks in this guide still apply, for example the void limit, a settled period and COST_PLUS_LINE. When a void goes through, the charge, when it is written, leaves the step out.
  • A step of a closed workflow. The void is refused with WORKFLOW_CLOSED. Voiding a step never voids the workflow’s charge, and Paid does not offer a way to void a step together with its charge.
  • The charge itself. You can void a workflow’s charge directly, like any signal. A charge carries _derived in its data. The charge is voided, the workflow’s steps stay live, and the workflow is not charged again. The same checks apply as for any signal, for example a settled period.
  • Late steps. Steps that arrive after the workflow closed, or after its charge was written, are handled as they were before voiding. Voiding adds no new refusal when you send signals.

Voiding does not void other signals for you, and it does not rebuild a charge from the steps that remain.