signalapp/Signal-Server · error · SubscriptionInvalidArgumentsException
invalid receipt credential request
Error message
invalid receipt credential request
What it means
createReceiptCredentials throws SubscriptionInvalidArgumentsException("invalid receipt credential request") when the client-supplied receiptCredentialRequest bytes cannot be parsed into a ReceiptCredentialRequest (InvalidInputException from the zk constructor). The payload is malformed, not merely wrong — it fails structural deserialization before any ZK verification.
Solutions
- Regenerate the ReceiptCredentialRequest client-side with a current, version-matched libsignal/zk receipt library and resend.
- Ensure the client sends the raw serialized bytes (correct base64 with padding intact) — no truncation, whitespace, or URL-safe alphabet mismatches.
- Log the byte length of the received payload and compare against the expected ReceiptCredentialRequest size to spot transport corruption.
Example fix
// before
String req = Base64.getEncoder().encodeToString(requestBytes).replace('+', '-'); // url-safe alphabet changed payload
// after
String req = Base64.getEncoder().encodeToString(new ReceiptCredentialRequest(serialized).serialize()); Defensive patterns
Strategy: validation
Validate before calling
byte[] decoded = Base64.getDecoder().decode(requestBase64);
if (decoded.length != new ReceiptCredentialRequest(validSample).serialize().length) {
throw new IllegalArgumentException("receiptCredentialRequest has unexpected byte length");
} Type guard
boolean isValidReceiptCredentialRequest(byte[] bytes) {
try { new ReceiptCredentialRequest(bytes); return true; } catch (InvalidInputException e) { return false; }
} Try / catch
try {
manager.createReceiptCredentials(creds, requestBytes);
} catch (SubscriptionInvalidArgumentsException e) {
log.warn("malformed receipt request — regenerate client-side");
} Prevention
- Keep client zk/libsignal library versions in sync with the server
- Use standard base64 (not URL-safe) and preserve padding through transports
- Sanity-check serialized payload length before sending
When it happens
Trigger: Posting a base64 receiptCredentialRequest body whose decoded bytes do not form a valid ReceiptCredentialRequest: wrong length, wrong serialization, garbage data, or bytes from a different protocol version.
Common situations: Client built the request with a mismatched libsignal/zk library version; the base64 payload got truncated or re-encoded by an intermediary; a test sent an arbitrary string instead of a real serialized request.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- 400 Bad Request (invalid ProfileKeyCommitment base64)
- receipt credential request failed verification
- Could not interpret identity key bytes as an EC public key
- Could not parse key as a base64-encoded value
- Could not interpret bytes as a ZK credential public key
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/b9879789a56b904f.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java:207
* @throws SubscriptionReceiptAlreadyRedeemedException if the receipt was already redeemed by a different
* request
* @throws RateLimitExceededException if rate-limited
*/
public ReceiptResult createReceiptCredentials(
final SubscriberCredentials subscriberCredentials,
final byte[] receiptCredentialRequestBytes,
final Function<CustomerAwareSubscriptionPaymentProcessor.ReceiptItem, Instant> expiration)
throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionInvalidArgumentsException, SubscriptionPaymentRequiredException, RateLimitExceededException, SubscriptionReceiptRequestedForOpenPaymentException, SubscriptionReceiptAlreadyRedeemedException {
final Subscriptions.Record record = getSubscriber(subscriberCredentials);
if (record.subscriptionId == null) {
throw new SubscriptionNotFoundException();
}
final ReceiptCredentialRequest receiptCredentialRequest;
try {
receiptCredentialRequest = new ReceiptCredentialRequest(receiptCredentialRequestBytes);
} catch (final InvalidInputException e) {
throw new SubscriptionInvalidArgumentsException("invalid receipt credential request", e);
}
final PaymentProvider processor = record.getProcessorCustomer().orElseThrow().processor();
final SubscriptionPaymentProcessor manager = getProcessor(processor);
final SubscriptionPaymentProcessor.ReceiptItem receipt = manager.getReceiptItem(record.subscriptionId);
final Instant expirationInstant = expiration.apply(receipt);
final ReceiptCredentialResponse receiptCredentialResponse;
try {
issuedReceiptsManager
.recordIssuance(receipt.itemId(), manager.getProvider(), receiptCredentialRequest, expirationInstant);
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
receiptCredentialRequest,
expirationInstant.getEpochSecond(),
receipt.level());
} catch (final VerificationFailedException e) {
throw new SubscriptionInvalidArgumentsException("receipt credential request failed verification", e);
} catch (final WriteConflictException _) {
throw new SubscriptionReceiptAlreadyRedeemedException();View on GitHub (pinned to 100ab61c82)