Subscribing to Webhooks

Register an endpoint, verify it, and choose which events reach it.

See Webhooks Overview first for delivery behavior, and the Event Catalog for the full list of event identifiers you can subscribe to.

1. Register an endpoint

RESPONSE=$(curl -s -X POST https://sandbox.api.thepennyinc.com/notifications/webhooks/ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/penny",
"trigger_events": ["card.issued", "card.terminated", "transaction.created"],
"authentication_token": "a-bearer-token-you-generate-and-check-for"
}')
ENDPOINT_ID=$(echo "$RESPONSE" | jq -r '.notification_endpoint_id')

authentication_token is optional. If set, Penny sends it as Authorization: Bearer <token> on each delivery; see Webhook Authorization. You can also pass filter_identifiers here; see Step 6.

2. Wait for provisioning

Creation is asynchronous: POST /notifications/webhooks/ returns 202 Accepted with "status": "pending_setup" and "updating": true while Penny provisions the delivery infrastructure. The webhook isn’t ready to verify yet. Poll GET /notifications/webhooks/{notification_endpoint_id} until status reaches pending_verification, then move on to Step 3.

until [ "$STATUS" = "pending_verification" ]; do
sleep 2
STATUS=$(curl -s "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.status')
if [ "$STATUS" = "setup_failed" ]; then echo "setup failed"; break; fi
done

status is one of nine values:

StatusMeaningWhat to do
pending_setupInfrastructure is being provisionedWait and poll
setup_failedProvisioning failedContact Penny Support
pending_verificationReady to verifyProceed to Step 3
verification_failedThe challenge wasn’t returned correctlyCheck verification.reason; re-trigger verification
updatingA configuration change is being appliedWait and poll
activeVerified and delivering eventsNormal operation
deactivatedManually pausedCall PATCH /notifications/webhooks/{notification_endpoint_id}/activate to resume
suspendedSuspended by PennyCheck suspension.reason
archivedArchived with DELETE; no longer deliversCreate a new webhook if needed

Both verification.reason (why setup or verification failed) and suspension.reason (why Penny suspended the webhook) are fields on the GET /notifications/webhooks/{notification_endpoint_id} response.

3. Verify the endpoint

A newly created webhook doesn’t deliver events until it’s verified. Verification confirms that you control the URL you registered.

POST /notifications/webhooks/{notification_endpoint_id}/verification sends a challenge to your URL. The challenge arrives like any other delivery, with "type": "endpoint.verification.pending" and "category": "verification"; its data.token is the verification code. Submit that code back with PATCH on the same path:

# Send a challenge to your endpoint
curl -X POST "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID/verification" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Submit the challenge's data.token as verification_code
curl -X PATCH "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID/verification" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"verification_code": "the-code-your-endpoint-received"}'

Only a verified, active webhook receives deliveries. Your receiver must accept the challenge request before verification completes, so handle endpoint.verification.pending before you check for subscribed event types.

4. Configure request signing (optional)

Webhook creation does not generate a signing secret. After provisioning finishes, create one:

POST /notifications/webhooks/{notification_endpoint_id}/signing_secret
{ "signing_secret": "penny_whsec_...", "algorithm": "HMAC-SHA256" }

Save the secret from this response. Webhook responses never return it, including with reveal_sensitive=true; has_signing_secret indicates whether signing is configured. Creating a second secret fails. To replace an existing secret, use POST /notifications/webhooks/{notification_endpoint_id}/signing_secret/rotate, which returns the same response shape. Rotation immediately changes the key used for new attempts.

Once a secret exists, each request carries Penny-Webhook-Timestamp and Penny-Webhook-Signature headers. Verify them using the entire secret string, including its prefix, as described in Verify Webhook Signatures.

5. Adjust which events it receives

Add or remove event identifiers without recreating the webhook:

curl -X PATCH "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID/trigger-events" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"trigger_events": ["cardholder.updated"]}'
curl -X DELETE "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID/trigger-events" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"trigger_events": ["transaction.created"]}'

6. Scope to specific objects (optional)

filter_identifiers narrows delivery to specific object IDs — for example, only transactions on one account, rather than every transaction on the business:

curl -X PUT "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID/filter-identifiers" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"filter_identifiers": ["account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9"]}'

7. Inspect delivery history

GET /notifications/webhooks/events lists your business’s events. The identifier query parameter is optional; pass an event type to return only events of that type. Delivery status lives on each delivery, not on the event, so list an event’s deliveries to see whether it reached your endpoint:

curl "https://sandbox.api.thepennyinc.com/notifications/webhooks/events?identifier=card.issued" \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl "https://sandbox.api.thepennyinc.com/notifications/webhooks/events/$NOTIFICATION_EVENT_ID/deliveries" \
-H "Authorization: Bearer $ACCESS_TOKEN"

GET /notifications/webhooks/events/{notification_event_id} returns a single event, including the exact payload that was delivered. GET /notifications/webhooks/{notification_endpoint_id}/deliveries lists deliveries to one endpoint. See Delivery status for the delivery fields and Delivery behavior for the retry schedule.

Pausing or archiving a webhook

curl -X PATCH "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID/deactivate" \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl -X DELETE "https://sandbox.api.thepennyinc.com/notifications/webhooks/$ENDPOINT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"

PATCH .../deactivate pauses deliveries and keeps the configuration; resume with PATCH .../activate. DELETE /notifications/webhooks/{notification_endpoint_id} archives the webhook and returns 202 Accepted. An archived webhook moves to archived status and no longer receives deliveries; create a new webhook if you need the URL again.

Managing the authentication token

authentication_token is write-only. Supply your own token when creating the webhook, or replace it with PATCH /notifications/webhooks/{notification_endpoint_id}. Omit the field to keep the existing token; send "authentication_token": null to remove it. Changing or removing the token requires ownership verification again.

Webhook responses expose has_authentication_token instead of the token. The token is never returned, including with reveal_sensitive=true or in history responses. Store your token before sending it to Penny; no token-retrieval endpoint is provided.