Webhook tests + FIXES + DOCS (#5686)

* webhook tests

* fixes and add docs

* Do not update FormResponse and StoreId in update/create PullPayment

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri
2024-02-23 09:44:42 +01:00
committed by GitHub
parent 10bb75ce0e
commit 5c98ca180a
9 changed files with 619 additions and 25 deletions

View File

@@ -7,11 +7,11 @@ namespace BTCPayServer.Client.Models
{ {
public class WebhookPayoutEvent : StoreWebhookEvent public class WebhookPayoutEvent : StoreWebhookEvent
{ {
public WebhookPayoutEvent(string evtType, string storeId) public WebhookPayoutEvent(string type, string storeId)
{ {
if (!evtType.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase)) if (!type.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType)); throw new ArgumentException("Invalid event type", nameof(type));
Type = evtType; Type = type;
StoreId = storeId; StoreId = storeId;
} }
@@ -21,11 +21,11 @@ namespace BTCPayServer.Client.Models
} }
public class WebhookPaymentRequestEvent : StoreWebhookEvent public class WebhookPaymentRequestEvent : StoreWebhookEvent
{ {
public WebhookPaymentRequestEvent(string evtType, string storeId) public WebhookPaymentRequestEvent(string type, string storeId)
{ {
if (!evtType.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase)) if (!type.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("Invalid event type", nameof(evtType)); throw new ArgumentException("Invalid event type", nameof(type));
Type = evtType; Type = type;
StoreId = storeId; StoreId = storeId;
} }

View File

@@ -2331,7 +2331,7 @@ namespace BTCPayServer.Tests
if (marked == InvoiceStatus.Settled) if (marked == InvoiceStatus.Settled)
{ {
Assert.Equal(InvoiceStatus.Settled, result.Status); Assert.Equal(InvoiceStatus.Settled, result.Status);
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled, await user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
o => o =>
{ {
Assert.Equal(inv.Id, o.InvoiceId); Assert.Equal(inv.Id, o.InvoiceId);
@@ -2341,7 +2341,7 @@ namespace BTCPayServer.Tests
if (marked == InvoiceStatus.Invalid) if (marked == InvoiceStatus.Invalid)
{ {
Assert.Equal(InvoiceStatus.Invalid, result.Status); Assert.Equal(InvoiceStatus.Invalid, result.Status);
var evt = user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid, var evt = await user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid,
o => o =>
{ {
Assert.Equal(inv.Id, o.InvoiceId); Assert.Equal(inv.Id, o.InvoiceId);

View File

@@ -452,9 +452,9 @@ namespace BTCPayServer.Tests
{ {
private Client.Models.StoreWebhookData _wh; private Client.Models.StoreWebhookData _wh;
private FakeServer _server; private FakeServer _server;
private readonly List<WebhookInvoiceEvent> _webhookEvents; private readonly List<StoreWebhookEvent> _webhookEvents;
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<WebhookInvoiceEvent> webhookEvents) public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<StoreWebhookEvent> webhookEvents)
{ {
_wh = wh; _wh = wh;
_server = server; _server = server;
@@ -472,7 +472,7 @@ namespace BTCPayServer.Tests
var callback = Encoding.UTF8.GetString(bytes); var callback = Encoding.UTF8.GetString(bytes);
lock (_webhookEvents) lock (_webhookEvents)
{ {
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback)); _webhookEvents.Add(JsonConvert.DeserializeObject<DummyStoreWebhookEvent>(callback));
} }
req.Response.StatusCode = 200; req.Response.StatusCode = 200;
_server.Done(); _server.Done();
@@ -485,8 +485,13 @@ namespace BTCPayServer.Tests
} }
} }
public List<WebhookInvoiceEvent> WebhookEvents { get; set; } = new List<WebhookInvoiceEvent>(); public class DummyStoreWebhookEvent : StoreWebhookEvent
public TEvent AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class {
}
public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>();
public async Task<TEvent> AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class
{ {
int retry = 0; int retry = 0;
retry: retry:
@@ -510,7 +515,7 @@ retry:
} }
if (retry < 3) if (retry < 3)
{ {
Thread.Sleep(1000); await Task.Delay(1000);
retry++; retry++;
goto retry; goto retry;
} }

View File

@@ -73,6 +73,9 @@ using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest; using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
using CreatePaymentRequestRequest = BTCPayServer.Client.Models.CreatePaymentRequestRequest;
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel; using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
@@ -1943,6 +1946,173 @@ namespace BTCPayServer.Tests
entity.GetPaymentMethods().First().Calculate(); entity.GetPaymentMethods().First().Calculate();
} }
[Fact()]
[Trait("Integration", "Integration")]
public async Task EnsureWebhooksTrigger()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetupWebhook();
var client = await user.CreateClient();
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.00m,
Currency = "BTC"
});;
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
//invoice payment webhooks
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.01m,
Currency = "BTC"
});
var invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
.PaymentLink, tester.ExplorerNode.Network);
var halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
.PaymentLink, tester.ExplorerNode.Network);
var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(remainingPaymentTx.ToString(), x.Payment.Id);
});
await tester.ExplorerNode.GenerateAsync(1);
await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(remainingPaymentTx.ToString(), x.Payment.Id);
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.01m,
Currency = "BTC",
});
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
.PaymentLink, tester.ExplorerNode.Network);
halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
(WebhookInvoiceReceivedPaymentEvent x) =>
{
Assert.Equal(invoice.Id, x.InvoiceId);
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
});
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0.01m,
Currency = "BTC"
});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Invalid});
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
//payment request webhook test
var pr = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest()
{
Amount = 100m,
Currency = "USD",
Title = "test pr",
//TODO: this is a bug, we should not have these props in create request
StoreId = user.StoreId,
FormResponse = new JObject(),
//END todo
Description = "lala baba"
});
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestCreated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
pr = await client.UpdatePaymentRequest(user.StoreId, pr.Id,
new UpdatePaymentRequestRequest() { Title = "test pr updated", Amount = 100m,
Currency = "USD",
//TODO: this is a bug, we should not have these props in create request
StoreId = user.StoreId,
FormResponse = new JObject(),
//END todo
Description = "lala baba"});
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestUpdated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
var inv = await client.PayPaymentRequest(user.StoreId, pr.Id, new PayPaymentRequestRequest() {});
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled});
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestStatusChanged, (WebhookPaymentRequestEvent x)=>
{
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, x.Status);
Assert.Equal(pr.Id, x.PaymentRequestId);
});
await client.ArchivePaymentRequest(user.StoreId, pr.Id);
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestArchived, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
//payoyt webhooks test
var payout = await client.CreatePayout(user.StoreId,
new CreatePayoutThroughStoreRequest()
{
Amount = 0.0001m,
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
Approved = true,
PaymentMethod = "BTC"
});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutCreated, (WebhookPayoutEvent x)=> Assert.Equal(payout.Id, x.PayoutId));
await client.MarkPayout(user.StoreId, payout.Id, new MarkPayoutRequest(){ State = PayoutState.AwaitingApproval});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutUpdated, (WebhookPayoutEvent x)=>
{
Assert.Equal(payout.Id, x.PayoutId);
Assert.Equal(PayoutState.AwaitingApproval, x.PayoutState);
});
await client.ApprovePayout(user.StoreId, payout.Id, new ApprovePayoutRequest(){});
await user.AssertHasWebhookEvent(WebhookEventType.PayoutApproved, (WebhookPayoutEvent x)=>
{
Assert.Equal(payout.Id, x.PayoutId);
Assert.Equal(PayoutState.AwaitingPayment, x.PayoutState);
});
await client.CancelPayout(user.StoreId, payout.Id );
await user.AssertHasWebhookEvent(WebhookEventType.PayoutUpdated, (WebhookPayoutEvent x)=>
{
Assert.Equal(payout.Id, x.PayoutId);
Assert.Equal(PayoutState.Cancelled, x.PayoutState);
});
}
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
public async Task InvoiceFlowThroughDifferentStatesCorrectly() public async Task InvoiceFlowThroughDifferentStatesCorrectly()

View File

@@ -168,6 +168,8 @@ namespace BTCPayServer.Controllers.Greenfield
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending, Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
Created = DateTimeOffset.UtcNow Created = DateTimeOffset.UtcNow
}; };
request.FormResponse = null;
request.StoreId = storeId;
pr.SetBlob(request); pr.SetBlob(request);
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr); pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
return Ok(FromModel(pr)); return Ok(FromModel(pr));
@@ -196,6 +198,9 @@ namespace BTCPayServer.Controllers.Greenfield
} }
var updatedPr = pr.First(); var updatedPr = pr.First();
var blob = updatedPr.GetBlob();
request.FormResponse = blob.FormResponse;
request.StoreId = storeId;
updatedPr.SetBlob(request); updatedPr.SetBlob(request);
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr))); return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));

View File

@@ -225,7 +225,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
if (payout.State != PayoutState.AwaitingPayment) if (payout.State != PayoutState.AwaitingPayment)
{ {
_eventAggregator.Publish(new PayoutEvent(null, payout)); _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
} }
} }
} }

View File

@@ -517,7 +517,7 @@ namespace BTCPayServer.HostedServices
} }
payout.State = req.Request.State; payout.State = req.Request.State;
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
_eventAggregator.Publish(new PayoutEvent(null, payout)); _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.Ok); req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.Ok);
} }
catch (Exception ex) catch (Exception ex)
@@ -739,7 +739,7 @@ namespace BTCPayServer.HostedServices
foreach (var keyValuePair in result.Where(pair => pair.Value == MarkPayoutRequest.PayoutPaidResult.Ok)) foreach (var keyValuePair in result.Where(pair => pair.Value == MarkPayoutRequest.PayoutPaidResult.Ok))
{ {
var payout = payouts.First(p => p.Id == keyValuePair.Key); var payout = payouts.First(p => p.Id == keyValuePair.Key);
_eventAggregator.Publish(new PayoutEvent(null, payout)); _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payout));
} }
cancel.Completion.TrySetResult(result); cancel.Completion.TrySetResult(result);
} }
@@ -959,7 +959,7 @@ namespace BTCPayServer.HostedServices
public JObject Metadata { get; set; } public JObject Metadata { get; set; }
} }
public record PayoutEvent(PayoutEvent.PayoutEventType? Type, PayoutData Payout) public record PayoutEvent(PayoutEvent.PayoutEventType Type, PayoutData Payout)
{ {
public enum PayoutEventType public enum PayoutEventType
{ {

View File

@@ -120,7 +120,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
foreach (var payoutData in payouts.Where(payoutData => payoutData.State != PayoutState.AwaitingPayment)) foreach (var payoutData in payouts.Where(payoutData => payoutData.State != PayoutState.AwaitingPayment))
{ {
_eventAggregator.Publish(new PayoutEvent(null, payoutData)); _eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payoutData));
} }
} }

View File

@@ -693,10 +693,10 @@
"nullable": false "nullable": false
}, },
"metadata": { "metadata": {
"type": "object", "type": "object",
"description": "User-supplied metadata added to the invoice at the time of its creation", "description": "User-supplied metadata added to the invoice at the time of its creation",
"nullable": false "nullable": false
} }
} }
} }
] ]
@@ -1129,6 +1129,420 @@
} }
} }
} }
},
"PaymentRequestCreated": {
"post": {
"operationId": "Webhook_PaymentRequestCreated",
"summary": "PaymentRequestCreated",
"description": "A new payment request has been created",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"paymentRequestId": {
"type": "string",
"description": "The id of the payment request",
"nullable": false
},
"status": {
"type": "string",
"enum": [
"Pending",
"Completed",
"Expired"
],
"description": "The status of the payment request",
"nullable": false
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"PaymentRequestUpdated": {
"post": {
"operationId": "Webhook_PaymentRequestUpdated",
"summary": "PaymentRequestUpdated",
"description": "A payment request has been updated",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"paymentRequestId": {
"type": "string",
"description": "The id of the payment request",
"nullable": false
},
"status": {
"type": "string",
"enum": [
"Pending",
"Completed",
"Expired"
],
"description": "The status of the payment request",
"nullable": false
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"PaymentRequestArchived": {
"post": {
"operationId": "Webhook_PaymentRequestArchved",
"summary": "PaymentRequestArchived",
"description": "A payment request has been archived",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"paymentRequestId": {
"type": "string",
"description": "The id of the payment request",
"nullable": false
},
"status": {
"type": "string",
"enum": [
"Pending",
"Completed",
"Expired"
],
"description": "The status of the payment request",
"nullable": false
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"PaymentRequestStatusChanged": {
"post": {
"operationId": "Webhook_PaymentRequestStatusChanged",
"summary": "PaymentRequestStatusChanged",
"description": "A payment request has had its status changed",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"paymentRequestId": {
"type": "string",
"description": "The id of the payment request",
"nullable": false
},
"status": {
"type": "string",
"enum": [
"Pending",
"Completed",
"Expired"
],
"description": "The status of the payment request",
"nullable": false
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"PayoutCreated": {
"post": {
"operationId": "Webhook_PayoutCreated",
"summary": "PayoutCreated",
"description": "A payout has been created",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"payoutId": {
"type": "string",
"description": "The id of the payout",
"nullable": false
},
"pullPaymentId": {
"type": "string",
"description": "The id of the pull payment this payout belongs to",
"nullable": true
},
"payoutState": {
"$ref": "#/components/schemas/PayoutState"
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"PayoutApproved": {
"post": {
"operationId": "Webhook_PayoutApproved",
"summary": "PayoutApproved",
"description": "A payout has been approved",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"payoutId": {
"type": "string",
"description": "The id of the payout",
"nullable": false
},
"pullPaymentId": {
"type": "string",
"description": "The id of the pull payment this payout belongs to",
"nullable": true
},
"payoutState": {
"$ref": "#/components/schemas/PayoutState"
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"PayoutUpdated": {
"post": {
"operationId": "Webhook_PayoutUpdated",
"summary": "PayoutUpdated",
"description": "A payout has been updated",
"parameters": [
{
"in": "header",
"name": "BTCPay-Sig",
"required": true,
"description": "The HMAC of the body's byte with the secret's of the webhook. `sha256=HMAC256(UTF8(webhook's secret), body)`",
"schema": {
"type": "string",
"example": "sha256=b438519edde5c8144a4f9bcec51a9d346eca6506887c2ceeae1c0092884a97b9"
}
}
],
"tags": [
"Webhooks"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"payoutId": {
"type": "string",
"description": "The id of the payout",
"nullable": false
},
"pullPaymentId": {
"type": "string",
"description": "The id of the pull payment this payout belongs to",
"nullable": true
},
"payoutState": {
"$ref": "#/components/schemas/PayoutState"
}
}
}
}
}
},
"responses": {
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
} }
}, },
"tags": [ "tags": [