On this page

Webhook Endpoints

Webhook endpoints give external services a place to deliver events: they accept incoming POST requests and store the JSON payload in a database table you configure. The endpoint validates the webhook ID against an allowed list, inserts the payload with a timestamp, and returns a success response. Nothing is parsed or transformed along the way; the raw payload is stored as-is, ready for downstream processing at your own pace.

Breaking change in v1.7.0

Webhooks are now namespaced endpoints. The single shared endpoints/Webhooks/entity.json and the flat route POST /api/{env}/webhook/{id} have been removed. Define each webhook under endpoints/Webhooks/{Namespace}/{Name}/entity.json and call it at POST /api/{env}/{namespace}/{name}/{id}. The old route now returns 410 Gone pointing at the new shape.

sequenceDiagram participant External as External Service participant Portway as Portway Gateway participant DB as SQL Database External->>Portway: POST /api/prod/Integrations/Inbound/payment-received Portway->>DB: INSERT INTO WebhookData DB-->>Portway: Success Portway-->>External: 200 OK

Downstream processing is handled by a separate job or procedure that reads from the webhook table. Portway does not retry failed inserts or forward payloads further.

Database setup

Create the webhook table before configuring the endpoint:

sql
CREATE TABLE [dbo].[WebhookData] (
    [Id]         INT IDENTITY(1,1) PRIMARY KEY,
    [WebhookId]  NVARCHAR(255)    NOT NULL,
    [Payload]    NVARCHAR(MAX)    NOT NULL,
    [ReceivedAt] DATETIME         NOT NULL DEFAULT GETDATE()
);

CREATE INDEX IX_WebhookData_WebhookId
ON [dbo].[WebhookData] ([WebhookId], [ReceivedAt] DESC);

If your processing job needs to track status, extend the table accordingly:

sql
ALTER TABLE WebhookData ADD
    ProcessedAt DATETIME     NULL,
    RetryCount  INT          NOT NULL DEFAULT 0,
    LastError   NVARCHAR(MAX) NULL;

Configuration

Create endpoints/Webhooks/Integrations/Inbound/entity.json (the Integrations folder is the namespace, Inbound the endpoint name):

json
{
  "DatabaseObjectName": "WebhookData",
  "DatabaseSchema": "dbo",
  "AllowedColumns": [
    "payment_webhook",
    "shipping_webhook",
    "inventory_webhook"
  ]
}

Configuration properties

Property Required Type Description
DatabaseObjectName Yes string Table name for storing webhook payloads
DatabaseSchema No string Database schema. Defaults to dbo
AllowedColumns No array Webhook IDs this endpoint accepts. Any ID not listed is rejected with 400

Webhook IDs map to values in the WebhookId column. Use names that identify the source and event type, stripe_payment_success, shopify_order_created, rather than generic identifiers.

Sending webhooks

txt
POST /api/{environment}/{namespace}/{name}/{webhookId}
http
POST /api/prod/Integrations/Inbound/payment-received
Content-Type: application/json
Authorization: Bearer <token>

{
  "event": "payment.success",
  "payment_id": "pay_123456",
  "amount": 99.99,
  "currency": "EUR",
  "timestamp": "2024-03-15T10:30:00Z"
}

Response:

json
{
  "success": true,
  "message": "Webhook processed successfully",
  "result": null,
  "id": 12345
}

WARNING

All webhook endpoints require Bearer token authentication. External services that do not support custom request headers cannot authenticate directly with Portway. For services that require unauthenticated inbound webhooks, place a proxy or ingress layer in front that adds the token before forwarding to Portway.

Querying stored payloads

Use SQL Server's JSON functions to extract fields from stored payloads:

sql
-- Recent payloads for one webhook type
SELECT TOP 10
    Id,
    JSON_VALUE(Payload, '$.event')      AS EventType,
    JSON_VALUE(Payload, '$.payment_id') AS PaymentId,
    ReceivedAt
FROM WebhookData
WHERE WebhookId = 'payment_webhook'
ORDER BY ReceivedAt DESC;

Limitations

  • POST only, webhook endpoints do not respond to GET, PUT, or DELETE
  • JSON only, payloads need to be valid JSON; non-JSON bodies are rejected
  • No payload validation beyond JSON syntax and webhook ID matching
  • No automatic retry on insert failure
  • Default payload size limit: 10MB

Troubleshooting

"Webhook ID not configured": The ID in the URL must match an entry in AllowedColumns exactly. Webhook IDs are case-sensitive.

Database connection errors: Verify the table exists with the correct schema and that the environment's connection string account has INSERT permission on the table.

Authentication failures: Confirm the Bearer token is valid and has access to the target environment.

To increase log verbosity:

json
{
  "Logging": {
    "LogLevel": {
      "PortwayApi.Api.EndpointController": "Debug"
    }
  }
}

Test with a minimal payload:

bash
curl -X POST https://your-api/api/prod/Integrations/Inbound/test_webhook \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'

Next steps

Last updated: 2026-07-23